diff --git a/ColorControl.sln b/ColorControl.sln index d2a632b..722c607 100644 --- a/ColorControl.sln +++ b/ColorControl.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Nspector", "Nspector\Nspect EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NvAPIWrapper", "NvAPIWrapper\NvAPIWrapper.csproj", "{42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "novideo_srgb", "novideo_srgb\novideo_srgb.csproj", "{A6A97834-7BE1-474A-B92F-A512DB1D5186}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,14 @@ Global {42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|Any CPU.Build.0 = Release|Any CPU {42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|x64.ActiveCfg = Release|Any CPU {42E2B4A6-6857-4427-AEF1-DF4C7EF8CC7C}.Release|x64.Build.0 = Release|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Debug|x64.Build.0 = Debug|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|Any CPU.Build.0 = Release|Any CPU + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|x64.ActiveCfg = Release|x64 + {A6A97834-7BE1-474A-B92F-A512DB1D5186}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ColorControl/ColorControl.csproj b/ColorControl/ColorControl.csproj index 7ab87c7..f6f2dbb 100644 --- a/ColorControl/ColorControl.csproj +++ b/ColorControl/ColorControl.csproj @@ -18,8 +18,8 @@ Maassoft Maassoft 0 - 9.4.0.0 - 9.4.0.0 + 9.5.0.0 + 9.5.0.0 false true false @@ -101,6 +101,7 @@ + diff --git a/ColorControl/Common/Utils.cs b/ColorControl/Common/Utils.cs index 227e93c..f16ef94 100644 --- a/ColorControl/Common/Utils.cs +++ b/ColorControl/Common/Utils.cs @@ -497,6 +497,21 @@ The best and suggested method to provide this is via a Windows Service. Only whe return null; // could also return string.Empty } + public static string RemoveFirstUnderscore(this string text) + { + if (string.IsNullOrEmpty(text)) + { + return text; + } + + if (text[0] == '_') + { + return text.Substring(1); + } + + return text; + } + public static string GetEnumNameByDescription(Type enumType, string description) { if (enumType.BaseType == typeof(Enum)) @@ -505,7 +520,7 @@ The best and suggested method to provide this is via a Windows Service. Only whe { if (description.Equals(GetDescription(enumType, enumValue as IConvertible), StringComparison.Ordinal)) { - return Enum.GetName(enumType, enumValue); + return Enum.GetName(enumType, enumValue).RemoveFirstUnderscore(); } } } @@ -879,7 +894,7 @@ The best and suggested method to provide this is via a Windows Service. Only whe public static FileInfo SelectFile(string ext = "*.exe", string filter = "Application EXE Name|*.exe|Application Absolute Path|*.exe") { - var openDialog = new OpenFileDialog(); + var openDialog = new System.Windows.Forms.OpenFileDialog(); openDialog.DefaultExt = ext; openDialog.Filter = filter; diff --git a/ColorControl/Forms/MessageForms.cs b/ColorControl/Forms/MessageForms.cs index 32062e7..0c5b359 100644 --- a/ColorControl/Forms/MessageForms.cs +++ b/ColorControl/Forms/MessageForms.cs @@ -150,21 +150,27 @@ namespace ColorControl.Forms var boxes = new List(); var counter = 1; const int DefaultLeft = 20; + var currentLeft = DefaultLeft; + + var columns = fields.Count() > 10 ? 2 : 1; + var currentColumn = 0; foreach (var field in fields) { + var lastTop = top; + var label = field.Label; if (field.FieldType != FieldType.CheckBox) { - var textLabel = new Label() { Left = DefaultLeft, Top = top, Text = label, AutoSize = true }; + var textLabel = new Label() { Left = currentLeft, Top = top, Text = label, AutoSize = true }; top += 20; groupBox.Controls.Add(textLabel); } if (field.SubLabel != null) { - var textSubLabel = new Label() { Left = DefaultLeft, Top = top, Text = field.SubLabel, AutoSize = true }; + var textSubLabel = new Label() { Left = currentLeft, Top = top, Text = field.SubLabel, AutoSize = true }; top += 20; groupBox.Controls.Add(textSubLabel); } @@ -174,7 +180,7 @@ namespace ColorControl.Forms switch (field.FieldType) { case FieldType.Text: - var textBox = new MaskedTextBox() { Left = DefaultLeft, Top = top, Width = 400 }; + var textBox = new MaskedTextBox() { Left = currentLeft, Top = top, Width = 400 }; if (label.Contains("Ip-address")) { @@ -194,7 +200,7 @@ namespace ColorControl.Forms case FieldType.Shortcut: var shortcutTextBox = new TextBox() { - Left = DefaultLeft, + Left = currentLeft, Top = top, Width = 200, Name = $"edtShortcut_{counter}", @@ -213,7 +219,7 @@ namespace ColorControl.Forms break; case FieldType.Numeric: - var numericEdit = new NumericUpDown() { Left = DefaultLeft, Top = top, Width = 200 }; + var numericEdit = new NumericUpDown() { Left = currentLeft, Top = top, Width = 200 }; if (field.MinValue != field.MaxValue) { @@ -232,7 +238,7 @@ namespace ColorControl.Forms case FieldType.DropDown: var comboBox = new ComboBox { - Left = DefaultLeft, + Left = currentLeft, Top = top, Width = 400, DropDownStyle = ComboBoxStyle.DropDownList @@ -258,7 +264,7 @@ namespace ColorControl.Forms var checkBox = new CheckBox { - Left = DefaultLeft, + Left = currentLeft, Top = top, AutoSize = true, Text = label @@ -272,7 +278,7 @@ namespace ColorControl.Forms case FieldType.Flags: var checkedListBox = new CheckedListBox { - Left = DefaultLeft, + Left = currentLeft, Top = top, Width = 400, Height = 100, @@ -300,7 +306,7 @@ namespace ColorControl.Forms break; case FieldType.TrackBar: - var trackBar = new TrackBar { Left = DefaultLeft, Top = top, Width = 300 }; + var trackBar = new TrackBar { Left = currentLeft, Top = top, Width = 300 }; var trackBarEdit = new NumericUpDown() { Left = trackBar.Left + trackBar.Width + 10, Top = top + 10, Width = 90 }; trackBarEdit.ValueChanged += prompt.TrackBarEdit_ValueChanged; @@ -349,14 +355,26 @@ namespace ColorControl.Forms continue; } - top += 30; + if (currentColumn + 1 < columns) + { + currentColumn++; + top = lastTop; + currentLeft = control.Left + control.Width + DefaultLeft; + } + else + { + top += 30; + currentColumn = 0; + currentLeft = DefaultLeft; + } + groupBox.Controls.Add(control); boxes.Add(control); control.Tag = field; counter++; } - var maxWidth = boxes.Max(c => c.Width + c.Left) + 20; + var maxWidth = boxes.Max(c => c.Width + c.Left) + 36; if (groupBox.Width < maxWidth) { diff --git a/ColorControl/MainForm.cs b/ColorControl/MainForm.cs index 8403ebe..92ed828 100644 --- a/ColorControl/MainForm.cs +++ b/ColorControl/MainForm.cs @@ -10,6 +10,7 @@ using ColorControl.Services.NVIDIA; using ColorControl.Svc; using Microsoft.Extensions.DependencyInjection; using NLog; +using novideo_srgb; using NvAPIWrapper.Display; using NWin32; using System; @@ -48,6 +49,7 @@ namespace ColorControl private LgPanel _lgPanel; private ToolStripMenuItem _nvTrayMenu; + private ToolStripMenuItem _novideoTrayMenu; private ToolStripMenuItem _amdTrayMenu; private ToolStripMenuItem _lgTrayMenu; private ToolStripMenuItem _gameTrayMenu; @@ -89,6 +91,7 @@ namespace ColorControl MessageForms.MainForm = this; _nvTrayMenu = new ToolStripMenuItem("NVIDIA presets"); + _novideoTrayMenu = new ToolStripMenuItem("Novideo sRGB"); _amdTrayMenu = new ToolStripMenuItem("AMD presets"); _lgTrayMenu = new ToolStripMenuItem("LG presets"); _gameTrayMenu = new ToolStripMenuItem("Game Launcher"); @@ -102,6 +105,7 @@ namespace ColorControl _trayIcon.ContextMenuStrip.Items.AddRange(new ToolStripItem[] { _nvTrayMenu, + _novideoTrayMenu, _amdTrayMenu, _lgTrayMenu, _gameTrayMenu, @@ -355,7 +359,7 @@ namespace ColorControl private void InitInfo() { - _currentVersionInfo = FileVersionInfo.GetVersionInfo(Path.GetFileName(Application.ExecutablePath)); + _currentVersionInfo = FileVersionInfo.GetVersionInfo(Application.ExecutablePath); Text = Application.ProductName + " " + Application.ProductVersion; @@ -374,6 +378,7 @@ namespace ColorControl lbPlugins.Items.Add("TaskScheduler by David Hall"); lbPlugins.Items.Add("NVIDIA Profile Inspector by Orbmu2k"); lbPlugins.Items.Add("NvidiaML wrapper by LibreHardwareMonitor"); + lbPlugins.Items.Add("Novideo sRGB by ledoge"); } private void OpenForm(object sender, EventArgs e) @@ -844,6 +849,12 @@ NOTE: installing the service may cause a User Account Control popup."); UpdateTrayMenu(_nvTrayMenu, presets, TrayMenuItemNv_Click); } + _novideoTrayMenu.Visible = _serviceManager.NvService != null && MainWindow.IsInitialized(); + if (_novideoTrayMenu.Visible) + { + MainWindow.UpdateContextMenu(_novideoTrayMenu); + } + _amdTrayMenu.Visible = _serviceManager.AmdService != null; if (_amdTrayMenu.Visible) { diff --git a/ColorControl/Services/LG/LgDevice.cs b/ColorControl/Services/LG/LgDevice.cs index 2646e57..b724c80 100644 --- a/ColorControl/Services/LG/LgDevice.cs +++ b/ColorControl/Services/LG/LgDevice.cs @@ -28,6 +28,7 @@ namespace ColorControl.Services.LG public bool Advanced { get; set; } public ModelYear FromModelYear { get; set; } = ModelYear.None; public ModelYear ToModelYear { get; set; } = ModelYear.None; + public List ValueLabels { get; set; } } public class LgDevicePictureSettings @@ -70,6 +71,8 @@ namespace ColorControl.Services.LG protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); public static List DefaultActionsOnGameBar = new() { "backlight", "contrast", "brightness", "color" }; + private static List IreValueLabels = new() { "IRE 2.5", "IRE 5", "IRE 7.5", "IRE 10", "IRE 15", "IRE 20", "IRE 25", "IRE 30", "IRE 35", "IRE 40", "IRE 45", "IRE 50", "IRE 55", "IRE 60", "IRE 65", "IRE 70", "IRE 75", "IRE 80", "IRE 85", "IRE 90", "IRE 95", "IRE 100" }; + public string Name { get; private set; } public string IpAddress { get; private set; } public string MacAddress { get; private set; } @@ -171,6 +174,7 @@ namespace ColorControl.Services.LG TriggersEnabled = true; AddInvokableAction("WOL", WakeAction); + AddInvokableAction("Reboot", RebootAction); AddGenericPictureAction("backlight", minValue: 0, maxValue: 100); AddGenericPictureAction("brightness", minValue: 0, maxValue: 100, title: "Brightness/Black Level"); AddGenericPictureAction("contrast", minValue: 0, maxValue: 100); @@ -214,47 +218,35 @@ namespace ColorControl.Services.LG AddGenericPictureAction("motionPro", typeof(OffToOn), title: "Motion Pro"); AddGenericPictureAction("realCinema", typeof(OffToOn), title: "Real Cinema"); - AddGenericPictureAction("uhdDeepColorHDMI1", typeof(OffToOn), category: "other"); - AddGenericPictureAction("uhdDeepColorHDMI2", typeof(OffToOn), category: "other"); - AddGenericPictureAction("uhdDeepColorHDMI3", typeof(OffToOn), category: "other"); - AddGenericPictureAction("uhdDeepColorHDMI4", typeof(OffToOn), category: "other"); + AddHdmiPictureAction("uhdDeepColorHDMI", typeof(OffToOn), category: "other"); AddGenericPictureAction("lowLevelAdjustment", minValue: -30, maxValue: 30, category: "other", title: "Fine Tune Dark Areas", fromModelYear: ModelYear.Series2019); AddGenericPictureAction("blackStabilizer", minValue: -30, maxValue: 30, category: "other", title: "Black Stabilizer", fromModelYear: ModelYear.Series2021); AddGenericPictureAction("whiteStabilizer", minValue: -30, maxValue: 30, category: "other", title: "White Stabilizer", fromModelYear: ModelYear.Series2021); AddGenericPictureAction("blueLight", typeof(BlueLight), category: "other", title: "Reduce Blue Light", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("gameMode_hdmi1", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI1", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("gameMode_hdmi2", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI2", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("gameMode_hdmi3", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI3", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("gameMode_hdmi4", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI4", fromModelYear: ModelYear.Series2021); + + AddHdmiPictureAction("gameMode_hdmi", typeof(OffToOn), category: "other", title: "Game Optimizer HDMI1", fromModelYear: ModelYear.Series2021); AddGenericPictureAction("gameOptimization", typeof(OffToOn), category: "other", title: "VRR & G-Sync", fromModelYear: ModelYear.Series2021); AddGenericPictureAction("inputOptimization", typeof(InputOptimization), category: "other", title: "Prevent Input Delay(Input Lag)", fromModelYear: ModelYear.Series2021); - AddGenericPictureAction("gameOptimizationHDMI1", typeof(OffToOn), category: "other"); - AddGenericPictureAction("gameOptimizationHDMI2", typeof(OffToOn), category: "other"); - AddGenericPictureAction("gameOptimizationHDMI3", typeof(OffToOn), category: "other"); - AddGenericPictureAction("gameOptimizationHDMI4", typeof(OffToOn), category: "other"); + AddHdmiPictureAction("gameOptimizationHDMI", typeof(OffToOn), category: "other"); AddGenericPictureAction("freesync", typeof(OffToOn), category: "other", title: "AMD FreeSync Premium", fromModelYear: ModelYear.Series2020); - AddGenericPictureAction("freesyncOLEDHDMI1", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); - AddGenericPictureAction("freesyncOLEDHDMI2", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); - AddGenericPictureAction("freesyncOLEDHDMI3", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); - AddGenericPictureAction("freesyncOLEDHDMI4", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("freesyncOLEDHDMI", typeof(OffToOn), category: "other", fromModelYear: ModelYear.Series2020); - AddGenericPictureAction("444BypassHDMI1", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMI1", fromModelYear: ModelYear.Series2023); - AddGenericPictureAction("444BypassHDMI2", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMI2", fromModelYear: ModelYear.Series2023); - AddGenericPictureAction("444BypassHDMI3", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMI3", fromModelYear: ModelYear.Series2023); - AddGenericPictureAction("444BypassHDMI4", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMI4", fromModelYear: ModelYear.Series2023); + AddHdmiPictureAction("444BypassHDMI", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through HDMIX", fromModelYear: ModelYear.Series2023); AddGenericPictureAction("444BypassHDMINone", typeof(OffToOn), category: "other", title: "4:4:4 Pass Through Non-HDMI", fromModelYear: ModelYear.Series2023); AddGenericPictureAction("qmsVrr", typeof(OffToOn), category: "other", title: "QMS-VRR", fromModelYear: ModelYear.Series2023); - //AddGenericPictureAction("freesyncSupport", typeof(OffToOn), category: "other"); - AddGenericPictureAction("hdmiPcMode_hdmi1", typeof(FalseToTrue), category: "other"); - AddGenericPictureAction("hdmiPcMode_hdmi2", typeof(FalseToTrue), category: "other"); - AddGenericPictureAction("hdmiPcMode_hdmi3", typeof(FalseToTrue), category: "other"); - AddGenericPictureAction("hdmiPcMode_hdmi4", typeof(FalseToTrue), category: "other"); - AddGenericPictureAction("adjustingLuminance", minValue: -50, maxValue: 50, numberOfValues: 22); - AddGenericPictureAction("whiteBalanceBlue", minValue: -50, maxValue: 50, numberOfValues: 22); - AddGenericPictureAction("whiteBalanceGreen", minValue: -50, maxValue: 50, numberOfValues: 22); - AddGenericPictureAction("whiteBalanceRed", minValue: -50, maxValue: 50, numberOfValues: 22); - //AddGenericPictureAction("wb20PointsGammaValue", minValue: -50, maxValue: 50); + AddGenericPictureAction("masterLuminanceLevel", typeof(MasterLuminanceLevel), category: "other"); + AddHdmiPictureAction("colorimetryHDMI", typeof(MasteringColor), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("masteringColorHDMI", typeof(MasteringColor), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("masteringPeakHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("maxCLLHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); + AddHdmiPictureAction("maxFALLHDMI", typeof(MasteringNits), category: "other", fromModelYear: ModelYear.Series2020); + + AddHdmiPictureAction("hdmiPcMode_hdmi", typeof(FalseToTrue), category: "other"); + AddGenericPictureAction("adjustingLuminance", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceBlue", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceGreen", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); + AddGenericPictureAction("whiteBalanceRed", minValue: -50, maxValue: 50, numberOfValues: 22, valueLabels: IreValueLabels); AddInvokableAction("turnScreenOff", TurnScreenOffAction); AddInvokableAction("turnScreenOn", TurnScreenOnAction); @@ -307,7 +299,17 @@ namespace ColorControl.Services.LG _invokableActions.Add(action); } - private void AddGenericPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None) + private void AddHdmiPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None) + { + for (var i = 1; i <= 4; i++) + { + var replacedTitle = title?.Replace("HDMIX", $"HDMI{i}"); + + AddGenericPictureAction($"{name}{i}", type, minValue, maxValue, category, replacedTitle, numberOfValues, fromModelYear); + } + } + + private void AddGenericPictureAction(string name, Type type = null, decimal minValue = 0, decimal maxValue = 0, string category = "picture", string title = null, int numberOfValues = 1, ModelYear fromModelYear = ModelYear.None, List valueLabels = null) { var action = new InvokableAction { @@ -319,7 +321,8 @@ namespace ColorControl.Services.LG NumberOfValues = numberOfValues, Category = category, Title = title == null ? Utils.FirstCharUpperCase(name) : title, - FromModelYear = fromModelYear + FromModelYear = fromModelYear, + ValueLabels = valueLabels }; _invokableActions.Add(action); @@ -1080,6 +1083,15 @@ namespace ColorControl.Services.LG return true; } + private async Task RebootAction(Dictionary _) + { + await CheckConnectionAsync(); + + await _lgTvApi.Reboot(); + + return true; + } + private async Task GenericPictureAction(Dictionary parameters) { await CheckConnectionAsync(); diff --git a/ColorControl/Services/LG/LgPanel.cs b/ColorControl/Services/LG/LgPanel.cs index 82c28fd..775f194 100644 --- a/ColorControl/Services/LG/LgPanel.cs +++ b/ColorControl/Services/LG/LgPanel.cs @@ -576,7 +576,7 @@ namespace ColorControl.Services.LG fields.AddRange(array.Select(i => new MessageForms.FieldDefinition { - Label = "Value for " + i, + Label = "Value for " + (action.ValueLabels != null ? action.ValueLabels[i] : i.ToString()), FieldType = MessageForms.FieldType.Numeric, MinValue = action.MinValue, MaxValue = action.MaxValue, diff --git a/ColorControl/Services/NVIDIA/NvPanel.Designer.cs b/ColorControl/Services/NVIDIA/NvPanel.Designer.cs index 6555710..5123a92 100644 --- a/ColorControl/Services/NVIDIA/NvPanel.Designer.cs +++ b/ColorControl/Services/NVIDIA/NvPanel.Designer.cs @@ -89,6 +89,7 @@ namespace ColorControl.Services.NVIDIA miNvProfileInspector = new System.Windows.Forms.ToolStripMenuItem(); miNvSettings = new System.Windows.Forms.ToolStripMenuItem(); lvNvPresetsToolTip = new System.Windows.Forms.ToolTip(components); + miNvNovideo = new System.Windows.Forms.ToolStripMenuItem(); mnuNvPresets.SuspendLayout(); mnuNvSettings.SuspendLayout(); SuspendLayout(); @@ -601,9 +602,9 @@ namespace ColorControl.Services.NVIDIA // mnuNvSettings // mnuNvSettings.ImageScalingSize = new System.Drawing.Size(20, 20); - mnuNvSettings.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { miNvProfileInspector, miNvSettings }); + mnuNvSettings.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { miNvProfileInspector, miNvNovideo, miNvSettings }); mnuNvSettings.Name = "mnuLgButtons"; - mnuNvSettings.Size = new System.Drawing.Size(202, 48); + mnuNvSettings.Size = new System.Drawing.Size(202, 92); // // miNvProfileInspector // @@ -619,6 +620,13 @@ namespace ColorControl.Services.NVIDIA miNvSettings.Text = "Settings"; miNvSettings.Click += miNvSettings_Click; // + // miNvNovideo + // + miNvNovideo.Name = "miNvNovideo"; + miNvNovideo.Size = new System.Drawing.Size(201, 22); + miNvNovideo.Text = "Novideo sRGB"; + miNvNovideo.Click += miNvNovideo_Click; + // // NvPanel // AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); @@ -648,7 +656,6 @@ namespace ColorControl.Services.NVIDIA Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); Name = "NvPanel"; Size = new System.Drawing.Size(1114, 539); - Load += RemoteControlPanel_Load; mnuNvPresets.ResumeLayout(false); mnuNvSettings.ResumeLayout(false); ResumeLayout(false); @@ -716,5 +723,6 @@ namespace ColorControl.Services.NVIDIA private System.Windows.Forms.TextBox edtNvOverclock; private System.Windows.Forms.ToolStripMenuItem mnuNvOverclocking; private System.Windows.Forms.ToolStripMenuItem miNvOverclockingIncluded; + private System.Windows.Forms.ToolStripMenuItem miNvNovideo; } } diff --git a/ColorControl/Services/NVIDIA/NvPanel.cs b/ColorControl/Services/NVIDIA/NvPanel.cs index 99efc1a..5098efc 100644 --- a/ColorControl/Services/NVIDIA/NvPanel.cs +++ b/ColorControl/Services/NVIDIA/NvPanel.cs @@ -2,6 +2,7 @@ using ColorControl.Forms; using ColorControl.Native; using ColorControl.Services.Common; +using novideo_srgb; using nspector; using nspector.Common; using nspector.Common.Meta; @@ -48,10 +49,13 @@ namespace ColorControl.Services.NVIDIA FormUtils.InitSortState(lvNvPresets, _config.NvPresetsSortState); _nvService.AfterApplyPreset += NvServiceAfterApplyPreset; - } - private void RemoteControlPanel_Load(object sender, EventArgs e) - { + MainViewModel.ConfigPath = _appContextProvider.GetAppContext().DataPath; + + if (_nvService.Config.ApplyNovideoOnStartup) + { + MainWindow.CreateAndShow(false); + } } private void lvLgPresets_ColumnClick(object sender, ColumnClickEventArgs e) @@ -562,7 +566,7 @@ namespace ColorControl.Services.NVIDIA var groupName = $"mnuNvDriverSettingGroup_{driverSetting.GroupName}"; var groupItem = FormUtils.BuildMenuItem(contextMenuStrip?.Items ?? parent.DropDownItems, groupName, driverSetting.GroupName); - var itemName = $"mnuNvDriverSettingItem_{driverSetting.SettingText}"; + var itemName = $"mnuNvDriverSettingItem_{driverSetting.SettingId}"; var item = FormUtils.BuildMenuItem(groupItem.DropDownItems, itemName, driverSetting.SettingText, driverSetting); var settingMeta = _nvService.GetSettingMeta(driverSetting.SettingId); @@ -618,11 +622,18 @@ namespace ColorControl.Services.NVIDIA if (settingMeta.SettingType == nspector.Native.NVAPI2.NVDRS_SETTING_TYPE.NVDRS_DWORD_TYPE) { + var index = 0; + foreach (var settingValue in settingMeta.DwordValues) { var text = settingValue.ValueName; if (text.IndexOf(" (") == 10) { + if (settingMeta.DwordValues.Take(index).Any(v => v.Value == settingValue.Value)) + { + continue; + } + text = text.Substring(0, 10); } @@ -631,6 +642,8 @@ namespace ColorControl.Services.NVIDIA subItem.Checked = presetSetting == settingValue.Value; subItem.Font = _menuItemFonts[subItem.Checked]; + + index++; } } @@ -1081,27 +1094,38 @@ namespace ColorControl.Services.NVIDIA private void EditNvSettings() { + var quickAccessField = new MessageForms.FieldDefinition + { + Label = "Quick Access shortcut", + FieldType = MessageForms.FieldType.Shortcut, + Value = _config.NvQuickAccessShortcut + }; + + var enableOcField = new MessageForms.FieldDefinition + { + Label = "Enable overclocking", + FieldType = MessageForms.FieldType.CheckBox, + Value = _nvService.Config.ShowOverclocking + }; + + var enableNovideoField = new MessageForms.FieldDefinition + { + Label = "Apply Novideo sRGB settings on startup", + FieldType = MessageForms.FieldType.CheckBox, + Value = _nvService.Config.ApplyNovideoOnStartup + }; + var values = MessageForms.ShowDialog("NVIDIA controller settings", new[] { - new MessageForms.FieldDefinition - { - Label = "Quick Access shortcut", - FieldType = MessageForms.FieldType.Shortcut, - Value = _config.NvQuickAccessShortcut - }, - new MessageForms.FieldDefinition - { - Label = "Enable overclocking", - FieldType = MessageForms.FieldType.CheckBox, - Value = _nvService.Config.ShowOverclocking - } + quickAccessField, + enableOcField, + enableNovideoField }); if (!values.Any()) { return; } - var shortcut = values[0].Value.ToString(); - var enableOc = (bool)values[1].Value; + var shortcut = (string)quickAccessField.Value; var clear = !string.IsNullOrEmpty(_config.NvQuickAccessShortcut); @@ -1109,7 +1133,8 @@ namespace ColorControl.Services.NVIDIA Utils.RegisterShortcut(_mainHandle, SHORTCUTID_NVQA, _config.NvQuickAccessShortcut, clear); - _nvService.Config.ShowOverclocking = enableOc; + _nvService.Config.ShowOverclocking = enableOcField.ValueAsBool; + _nvService.Config.ApplyNovideoOnStartup = enableNovideoField.ValueAsBool; UpdateInfo(); } @@ -1499,5 +1524,10 @@ namespace ColorControl.Services.NVIDIA { UpdateDisplayInfoItems(); } + + private void miNvNovideo_Click(object sender, EventArgs e) + { + MainWindow.CreateAndShow(); + } } } diff --git a/ColorControl/Services/NVIDIA/NvServiceConfig.cs b/ColorControl/Services/NVIDIA/NvServiceConfig.cs index 9ab9780..6934993 100644 --- a/ColorControl/Services/NVIDIA/NvServiceConfig.cs +++ b/ColorControl/Services/NVIDIA/NvServiceConfig.cs @@ -3,6 +3,7 @@ class NvServiceConfig { public bool ShowOverclocking { get; set; } + public bool ApplyNovideoOnStartup { get; set; } public NvServiceConfig() { diff --git a/ColorControl/lgtv/LgTvApi.cs b/ColorControl/lgtv/LgTvApi.cs index f2fe6f9..b7a48d8 100644 --- a/ColorControl/lgtv/LgTvApi.cs +++ b/ColorControl/lgtv/LgTvApi.cs @@ -269,6 +269,49 @@ namespace LgTv wisa_speaker, } + public enum MasterLuminanceLevel + { + [Description("540 nits")] + _540nit, + [Description("1000 nits")] + _1000nit, + [Description("4000 nits")] + _4000nit + } + + public enum MasteringColor + { + auto, + [Description("0")] + _0, + p3D65, + bt2020D65, + bt709D65, + } + + public enum MasteringNits + { + auto, + [Description("0")] + _0, + [Description("400")] + _400, + [Description("540")] + _540, + [Description("700")] + _700, + [Description("1000")] + _1000, + [Description("2000")] + _2000, + [Description("3000")] + _3000, + [Description("4000")] + _4000, + [Description("10000")] + _10000, + } + public class LgTvApi : IDisposable { public bool ConnectionClosed { get => _connection?.ConnectionClosed ?? true; } @@ -435,6 +478,7 @@ namespace LgTv { await SetMute(!await IsMuted()); } + public async Task IsMuted() { var requestMessage = new RequestMessage("status", "ssap://audio/getStatus"); @@ -454,6 +498,7 @@ namespace LgTv { await _connection.SendCommandAsync(new RequestMessage("", "ssap://system/turnOff")); } + public async Task Reboot() { await ExecuteRequest("luna://com.webos.service.tv.power/reboot", new { reason = "" }); @@ -704,6 +749,10 @@ namespace LgTv string jsonValue; var valueType = value.GetType(); var intType = typeof(int); + if (value.ToString().StartsWith("_")) + { + value = value.ToString().Substring(1); + } if (value.ToString().StartsWith("bool_")) { value = bool.Parse(value.ToString().Substring(5)); diff --git a/novideo_srgb/AboutWindow.xaml b/novideo_srgb/AboutWindow.xaml new file mode 100644 index 0000000..b91f4c3 --- /dev/null +++ b/novideo_srgb/AboutWindow.xaml @@ -0,0 +1,22 @@ + + + + novideo_srgb v4.0 by ledoge + + + Licensed under GPLv3 + Source code and releases hosted at + https://github.com/ledoge/novideo_srgb + + + \ No newline at end of file diff --git a/novideo_srgb/AboutWindow.xaml.cs b/novideo_srgb/AboutWindow.xaml.cs new file mode 100644 index 0000000..f3143e5 --- /dev/null +++ b/novideo_srgb/AboutWindow.xaml.cs @@ -0,0 +1,22 @@ +using System.Windows; +using System.Windows.Navigation; + +namespace novideo_srgb +{ + public partial class AboutWindow : Window + { + public AboutWindow() + { + InitializeComponent(); + } + + private void OnRequestNavigate(object sender, RequestNavigateEventArgs e) + { + var processStartInfo = new System.Diagnostics.ProcessStartInfo(e.Uri.AbsoluteUri) + { + UseShellExecute = true, + }; + System.Diagnostics.Process.Start(processStartInfo); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/AdvancedViewModel.cs b/novideo_srgb/AdvancedViewModel.cs new file mode 100644 index 0000000..01f25d9 --- /dev/null +++ b/novideo_srgb/AdvancedViewModel.cs @@ -0,0 +1,188 @@ +using EDIDParser; +using System.ComponentModel; +using System.IO; +using System.Runtime.CompilerServices; +using System.Windows; + +namespace novideo_srgb +{ + public class AdvancedViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + private MonitorData _monitor; + + private int _target; + private bool _useIcc; + private string _profilePath; + private bool _calibrateGamma; + private int _selectedGamma; + private double _customGamma; + private double _customPercentage; + private bool _disableOptimization; + + public AdvancedViewModel() + { + throw new NotSupportedException(); + } + + public AdvancedViewModel(MonitorData monitor) + { + _monitor = monitor; + + _target = monitor.Target; + _useIcc = monitor.UseIcc; + _profilePath = monitor.ProfilePath; + _calibrateGamma = monitor.CalibrateGamma; + _selectedGamma = monitor.SelectedGamma; + _customGamma = monitor.CustomGamma; + _customPercentage = monitor.CustomPercentage; + _disableOptimization = monitor.DisableOptimization; + } + + public void ApplyChanges() + { + ChangedCalibration |= _monitor.Target != _target; + _monitor.Target = _target; + ChangedCalibration |= _monitor.UseIcc != _useIcc; + _monitor.UseIcc = _useIcc; + ChangedCalibration |= _monitor.ProfilePath != _profilePath; + _monitor.ProfilePath = _profilePath; + ChangedCalibration |= _monitor.CalibrateGamma != _calibrateGamma; + _monitor.CalibrateGamma = _calibrateGamma; + ChangedCalibration |= _monitor.SelectedGamma != _selectedGamma; + _monitor.SelectedGamma = _selectedGamma; + ChangedCalibration |= _monitor.CustomGamma != _customGamma; + _monitor.CustomGamma = _customGamma; + ChangedCalibration |= _monitor.CustomPercentage != _customPercentage; + _monitor.CustomPercentage = _customPercentage; + ChangedCalibration |= _monitor.DisableOptimization != _disableOptimization; + _monitor.DisableOptimization = _disableOptimization; + } + + public ChromaticityCoordinates Coords => _monitor.Edid.DisplayParameters.ChromaticityCoordinates; + + public bool UseEdid + { + set + { + if (!value == _useIcc) return; + _useIcc = !value; + OnPropertyChanged(); + OnPropertyChanged(nameof(UseIcc)); + OnPropertyChanged(nameof(EdidWarning)); + } + get => !_useIcc; + } + + public bool UseIcc + { + set + { + if (value == _useIcc) return; + _useIcc = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(UseEdid)); + OnPropertyChanged(nameof(EdidWarning)); + } + get => _useIcc; + } + + public string ProfilePath + { + set + { + if (value == _profilePath) return; + _profilePath = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ProfileName)); + } + get => _profilePath; + } + + public string ProfileName => Path.GetFileName(ProfilePath); + + public bool CalibrateGamma + { + set + { + if (value == _calibrateGamma) return; + _calibrateGamma = value; + OnPropertyChanged(); + } + get => _calibrateGamma; + } + + public int SelectedGamma + { + set + { + if (value == _selectedGamma) return; + _selectedGamma = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(UseCustomGamma)); + } + get => _selectedGamma; + } + + public Visibility UseCustomGamma => + SelectedGamma == 2 || SelectedGamma == 3 ? Visibility.Visible : Visibility.Collapsed; + + public double CustomGamma + { + set + { + if (value == _customGamma) return; + _customGamma = value; + OnPropertyChanged(); + } + get => _customGamma; + } + + public int Target + { + set + { + if (value == _target) return; + _target = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(EdidWarning)); + } + get => _target; + } + + public Visibility HdrWarning => _monitor.HdrActive ? Visibility.Visible : Visibility.Collapsed; + public Visibility EdidWarning => HdrWarning != Visibility.Visible && UseEdid && Colorimetry.ColorSpaces[_target].Equals(_monitor.EdidColorSpace) + ? Visibility.Visible + : Visibility.Collapsed; + + public double CustomPercentage + { + set + { + if (value == _customPercentage) return; + _customPercentage = value; + OnPropertyChanged(); + } + get => _customPercentage; + } + + public bool DisableOptimization + { + set + { + if (value == _disableOptimization) return; + _disableOptimization = value; + OnPropertyChanged(); + } + get => _disableOptimization; + } + + public bool ChangedCalibration { get; set; } + + private void OnPropertyChanged([CallerMemberName] string name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/AdvancedWindow.xaml b/novideo_srgb/AdvancedWindow.xaml new file mode 100644 index 0000000..b9ddd1d --- /dev/null +++ b/novideo_srgb/AdvancedWindow.xaml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + sRGB/BT.709 + Display P3 + Adobe RGB + BT.2020 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Primaries match target – cannot clamp + HDR is active – cannot clamp + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novideo_srgb/AdvancedWindow.xaml.cs b/novideo_srgb/AdvancedWindow.xaml.cs new file mode 100644 index 0000000..cbad16e --- /dev/null +++ b/novideo_srgb/AdvancedWindow.xaml.cs @@ -0,0 +1,45 @@ +using System.Windows; + +namespace novideo_srgb +{ + public partial class AdvancedWindow + { + private AdvancedViewModel _viewModel; + + public AdvancedWindow(MonitorData monitor) + { + _viewModel = new AdvancedViewModel(monitor); + DataContext = _viewModel; + InitializeComponent(); + } + + private static string BrowseProfiles() + { + var dlg = new Microsoft.Win32.OpenFileDialog + { + Filter = "ICC Profiles|*.icc;*.icm" + }; + + var result = dlg.ShowDialog(); + + return result == true ? dlg.FileName : null; + } + + private void Browse_Click(object sender, RoutedEventArgs e) + { + var profilePath = BrowseProfiles(); + if (!string.IsNullOrEmpty(profilePath)) + { + _viewModel.ProfilePath = profilePath; + } + } + + private void OK_Click(object sender, RoutedEventArgs e) + { + _viewModel.ApplyChanges(); + DialogResult = true; + } + + public bool ChangedCalibration => _viewModel.ChangedCalibration; + } +} \ No newline at end of file diff --git a/novideo_srgb/Colorimetry.cs b/novideo_srgb/Colorimetry.cs new file mode 100644 index 0000000..28a35af --- /dev/null +++ b/novideo_srgb/Colorimetry.cs @@ -0,0 +1,180 @@ +// credit to https://mina86.com/2019/srgb-xyz-matrix/ and http://www.brucelindbloom.com/ for the math + +namespace novideo_srgb +{ + public static class Colorimetry + { + public struct Point + { + public bool Equals(Point other) + { + return X.Equals(other.X) && Y.Equals(other.Y); + } + + public override bool Equals(object obj) + { + return obj is Point other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + return (X.GetHashCode() * 397) ^ Y.GetHashCode(); + } + } + + public double X; + public double Y; + } + + public struct ColorSpace + { + public bool Equals(ColorSpace other) + { + return Red.Equals(other.Red) && Green.Equals(other.Green) && Blue.Equals(other.Blue) && + White.Equals(other.White); + } + + public override bool Equals(object obj) + { + return obj is ColorSpace other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Red.GetHashCode(); + hashCode = (hashCode * 397) ^ Green.GetHashCode(); + hashCode = (hashCode * 397) ^ Blue.GetHashCode(); + hashCode = (hashCode * 397) ^ White.GetHashCode(); + return hashCode; + } + } + + public Point Red; + public Point Green; + public Point Blue; + public Point White; + } + + public static Point D65 = new Point { X = 0.3127, Y = 0.3290 }; + + public static ColorSpace sRGB = new ColorSpace + { + Red = new Point { X = 0.64, Y = 0.33 }, + Green = new Point { X = 0.3, Y = 0.6 }, + Blue = new Point { X = 0.15, Y = 0.06 }, + White = D65 + }; + + public static ColorSpace DisplayP3 = new ColorSpace + { + Red = new Point { X = 0.68, Y = 0.32 }, + Green = new Point { X = 0.265, Y = 0.69 }, + Blue = new Point { X = 0.15, Y = 0.06 }, + White = D65 + }; + + public static ColorSpace AdobeRGB = new ColorSpace + { + Red = new Point { X = 0.64, Y = 0.33 }, + Green = new Point { X = 0.21, Y = 0.71 }, + Blue = new Point { X = 0.15, Y = 0.06 }, + White = D65 + }; + + public static ColorSpace BT2020 = new ColorSpace + { + Red = new Point { X = 0.708, Y = 0.292 }, + Green = new Point { X = 0.17, Y = 0.797 }, + Blue = new Point { X = 0.131, Y = 0.046 }, + White = D65 + }; + + public static ColorSpace[] ColorSpaces => new[] { sRGB, DisplayP3, AdobeRGB, BT2020 }; + + public static Matrix D50 = Matrix.FromValues(new[,] { { 0.9642 }, { 1 }, { 0.8249 } }); + + public static Matrix RGBToXYZ(ColorSpace colorSpace) + { + var red = colorSpace.Red; + var green = colorSpace.Green; + var blue = colorSpace.Blue; + var white = colorSpace.White; + var whiteXYZ = Matrix.FromValues(new[,] + { { white.X / white.Y }, { 1 }, { (1 - white.X - white.Y) / white.Y } }); + + var Mprime = Matrix.FromValues(new[,] + { + { red.X / red.Y, green.X / green.Y, blue.X / blue.Y }, + { 1, 1, 1 }, + { (1 - red.X - red.Y) / red.Y, (1 - green.X - green.Y) / green.Y, (1 - blue.X - blue.Y) / blue.Y } + }); + + return Mprime * Matrix.FromDiagonal(Mprime.Inverse() * whiteXYZ); + } + + public static Matrix XYZToRGB(ColorSpace colorSpace) + { + return RGBToXYZ(colorSpace).Inverse(); + } + + public static Matrix RGBToRGB(ColorSpace from, ColorSpace to) + { + var result = XYZToRGB(to) * RGBToXYZ(from); + return result; + } + + public static Matrix RGBToAdaptedXYZ(ColorSpace colorspace, Matrix whiteXYZ) + { + var xyz = RGBToXYZ(colorspace); + var bradford = Matrix.FromValues(new[,] + { + { 0.8951, 0.2664, -0.1614 }, + { -0.7502, 1.7135, 0.0367 }, + { 0.0389, -0.0685, 1.0296 } + }); + var ws = colorspace.White; + var aws = bradford * Matrix.FromValues(new[,] + { + { ws.X / ws.Y }, { 1 }, { (1 - ws.X - ws.Y) / ws.Y } + }); + var awd = bradford * whiteXYZ; + var m = bradford.Inverse() * Matrix.FromDiagonal(new[] + { awd[0] / aws[0], awd[1] / aws[1], awd[2] / aws[2] }) * bradford; + return m * xyz; + } + + public static Matrix RGBToPCSXYZ(ColorSpace colorspace) + { + return RGBToAdaptedXYZ(colorspace, D50); + } + + public static Matrix PCSXYZToRGB(ColorSpace colorspace) + { + return RGBToPCSXYZ(colorspace).Inverse(); + } + + public static Matrix XYZScale(Matrix matrix, Matrix target) + { + var result = Matrix.Zero3x3(); + var white = matrix * Matrix.One3x1(); + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 3; j++) + { + result[i, j] = matrix[i, j] * target[i] / white[i]; + } + } + + return result; + } + + public static Matrix XYZScaleToD50(Matrix matrix) + { + return XYZScale(matrix, D50); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/DisplayConfigManager.cs b/novideo_srgb/DisplayConfigManager.cs new file mode 100644 index 0000000..7b5b255 --- /dev/null +++ b/novideo_srgb/DisplayConfigManager.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; + +namespace novideo_srgb +{ + public static class DisplayConfigManager + { + [DllImport("user32")] + private static extern int GetDisplayConfigBufferSizes(QDC flags, out int numPathArrayElements, out int numModeInfoArrayElements); + + [DllImport("user32")] + private static extern int QueryDisplayConfig(QDC flags, ref int numPathArrayElements, [In, Out] DISPLAYCONFIG_PATH_INFO[] pathArray, ref int numModeInfoArrayElements, [In, Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray, out DISPLAYCONFIG_TOPOLOGY_ID currentTopologyId); + + [DllImport("user32")] + private static extern int QueryDisplayConfig(QDC flags, ref int numPathArrayElements, [In, Out] DISPLAYCONFIG_PATH_INFO[] pathArray, ref int numModeInfoArrayElements, [In, Out] DISPLAYCONFIG_MODE_INFO[] modeInfoArray, IntPtr currentTopologyId); + + [DllImport("user32")] + private static extern int DisplayConfigGetDeviceInfo(ref DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO requestPacket); + + [DllImport("user32")] + private static extern int DisplayConfigGetDeviceInfo(ref DISPLAYCONFIG_SOURCE_DEVICE_NAME requestPacket); + + [DllImport("user32")] + private static extern int DisplayConfigGetDeviceInfo(ref DISPLAYCONFIG_TARGET_DEVICE_NAME requestPacket); + + public static HashSet GetHdrDisplayPaths() + { + Action check = (e) => + { + if (e != 0) + { + throw new Win32Exception(e); + } + }; + + check(GetDisplayConfigBufferSizes(QDC.QDC_ONLY_ACTIVE_PATHS, out var pathCount, out var modeCount)); + + var paths = new DISPLAYCONFIG_PATH_INFO[pathCount]; + var modes = new DISPLAYCONFIG_MODE_INFO[modeCount]; + + check(QueryDisplayConfig(QDC.QDC_ONLY_ACTIVE_PATHS, ref pathCount, paths, ref modeCount, modes, IntPtr.Zero)); + + var result = new HashSet(); + + Array.ForEach(paths, path => + { + var displayInfo = new DISPLAYCONFIG_TARGET_DEVICE_NAME(); + displayInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + displayInfo.header.size = Marshal.SizeOf(); + displayInfo.header.adapterId = path.targetInfo.adapterId; + displayInfo.header.id = path.targetInfo.id; + + check(DisplayConfigGetDeviceInfo(ref displayInfo)); + + var colorInfo = new DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO(); + colorInfo.header.type = DISPLAYCONFIG_DEVICE_INFO_TYPE.DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO; + colorInfo.header.size = Marshal.SizeOf(); + colorInfo.header.adapterId = path.targetInfo.adapterId; + colorInfo.header.id = path.targetInfo.id; + + check(DisplayConfigGetDeviceInfo(ref colorInfo)); + + if (colorInfo.advancedColorEnabled) + { + result.Add(displayInfo.monitorDevicePath); + } + }); + + return result; + } + } + + internal enum DISPLAYCONFIG_DEVICE_INFO_TYPE + { + DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME = 1, + DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME = 2, + DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE = 3, + DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME = 4, + DISPLAYCONFIG_DEVICE_INFO_SET_TARGET_PERSISTENCE = 5, + DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_BASE_TYPE = 6, + DISPLAYCONFIG_DEVICE_INFO_GET_SUPPORT_VIRTUAL_RESOLUTION = 7, + DISPLAYCONFIG_DEVICE_INFO_SET_SUPPORT_VIRTUAL_RESOLUTION = 8, + DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO = 9, + DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE = 10, + DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL = 11, + } + + internal enum DISPLAYCONFIG_COLOR_ENCODING + { + DISPLAYCONFIG_COLOR_ENCODING_RGB = 0, + DISPLAYCONFIG_COLOR_ENCODING_YCBCR444 = 1, + DISPLAYCONFIG_COLOR_ENCODING_YCBCR422 = 2, + DISPLAYCONFIG_COLOR_ENCODING_YCBCR420 = 3, + DISPLAYCONFIG_COLOR_ENCODING_INTENSITY = 4, + } + + internal enum DISPLAYCONFIG_SCALING + { + DISPLAYCONFIG_SCALING_IDENTITY = 1, + DISPLAYCONFIG_SCALING_CENTERED = 2, + DISPLAYCONFIG_SCALING_STRETCHED = 3, + DISPLAYCONFIG_SCALING_ASPECTRATIOCENTEREDMAX = 4, + DISPLAYCONFIG_SCALING_CUSTOM = 5, + DISPLAYCONFIG_SCALING_PREFERRED = 128, + } + + internal enum DISPLAYCONFIG_ROTATION + { + DISPLAYCONFIG_ROTATION_IDENTITY = 1, + DISPLAYCONFIG_ROTATION_ROTATE90 = 2, + DISPLAYCONFIG_ROTATION_ROTATE180 = 3, + } + + internal enum DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY + { + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_OTHER = -1, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HD15 = 0, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SVIDEO = 1, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPOSITE_VIDEO = 2, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_COMPONENT_VIDEO = 3, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DVI = 4, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI = 5, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_LVDS = 6, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_D_JPN = 8, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDI = 9, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL = 10, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EMBEDDED = 11, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EXTERNAL = 12, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_UDI_EMBEDDED = 13, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_SDTVDONGLE = 14, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_MIRACAST = 15, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_WIRED = 16, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INDIRECT_VIRTUAL = 17, + DISPLAYCONFIG_OUTPUT_TECHNOLOGY_INTERNAL = unchecked((int)0x80000000), + } + + internal enum DISPLAYCONFIG_TOPOLOGY_ID + { + DISPLAYCONFIG_TOPOLOGY_INTERNAL = 0x00000001, + DISPLAYCONFIG_TOPOLOGY_CLONE = 0x00000002, + DISPLAYCONFIG_TOPOLOGY_EXTEND = 0x00000004, + DISPLAYCONFIG_TOPOLOGY_EXTERNAL = 0x00000008, + } + + internal enum DISPLAYCONFIG_PATH + { + DISPLAYCONFIG_PATH_ACTIVE = 0x00000001, + DISPLAYCONFIG_PATH_PREFERRED_UNSCALED = 0x00000004, + DISPLAYCONFIG_PATH_SUPPORT_VIRTUAL_MODE = 0x00000008, + } + + internal enum DISPLAYCONFIG_SOURCE_FLAGS + { + DISPLAYCONFIG_SOURCE_IN_USE = 0x00000001, + } + + internal enum DISPLAYCONFIG_TARGET_FLAGS + { + DISPLAYCONFIG_TARGET_IN_USE = 0x00000001, + DISPLAYCONFIG_TARGET_FORCIBLE = 0x00000002, + DISPLAYCONFIG_TARGET_FORCED_AVAILABILITY_BOOT = 0x00000004, + DISPLAYCONFIG_TARGET_FORCED_AVAILABILITY_PATH = 0x00000008, + DISPLAYCONFIG_TARGET_FORCED_AVAILABILITY_SYSTEM = 0x00000010, + DISPLAYCONFIG_TARGET_IS_HMD = 0x00000020, + } + + internal enum QDC + { + QDC_ALL_PATHS = 0x00000001, + QDC_ONLY_ACTIVE_PATHS = 0x00000002, + QDC_DATABASE_CURRENT = 0x00000004, + QDC_VIRTUAL_MODE_AWARE = 0x00000010, + QDC_INCLUDE_HMD = 0x00000020, + } + + internal enum DISPLAYCONFIG_SCANLINE_ORDERING + { + DISPLAYCONFIG_SCANLINE_ORDERING_UNSPECIFIED = 0, + DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE = 1, + DISPLAYCONFIG_SCANLINE_ORDERING_INTERLACED = 2, + DISPLAYCONFIG_SCANLINE_ORDERING_INTERLACED_UPPERFIELDFIRST = DISPLAYCONFIG_SCANLINE_ORDERING_INTERLACED, + DISPLAYCONFIG_SCANLINE_ORDERING_INTERLACED_LOWERFIELDFIRST = 3, + } + + internal enum DISPLAYCONFIG_PIXELFORMAT + { + DISPLAYCONFIG_PIXELFORMAT_8BPP = 1, + DISPLAYCONFIG_PIXELFORMAT_16BPP = 2, + DISPLAYCONFIG_PIXELFORMAT_24BPP = 3, + DISPLAYCONFIG_PIXELFORMAT_32BPP = 4, + DISPLAYCONFIG_PIXELFORMAT_NONGDI = 5, + } + + internal enum DISPLAYCONFIG_MODE_INFO_TYPE + { + DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE = 1, + DISPLAYCONFIG_MODE_INFO_TYPE_TARGET = 2, + DISPLAYCONFIG_MODE_INFO_TYPE_DESKTOP_IMAGE = 3, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_DEVICE_INFO_HEADER + { + public DISPLAYCONFIG_DEVICE_INFO_TYPE type; + public int size; + public LUID adapterId; + public uint id; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO + { + public DISPLAYCONFIG_DEVICE_INFO_HEADER header; + public uint value; + public DISPLAYCONFIG_COLOR_ENCODING colorEncoding; + public int bitsPerColorChannel; + + public bool advancedColorSupported => (value & 0x1) == 0x1; + public bool advancedColorEnabled => (value & 0x2) == 0x2; + public bool wideColorEnforced => (value & 0x4) == 0x4; + public bool advancedColorForceDisabled => (value & 0x8) == 0x8; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct POINTL + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct LUID + { + public uint LowPart; + public int HighPart; + + public long Value => ((long)HighPart << 32) | LowPart; + public override string ToString() => Value.ToString(); + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_SOURCE_MODE + { + public uint width; + public uint height; + public DISPLAYCONFIG_PIXELFORMAT pixelFormat; + public POINTL position; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_RATIONAL + { + public uint Numerator; + public uint Denominator; + + public override string ToString() => Numerator + " / " + Denominator; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_2DREGION + { + public uint cx; + public uint cy; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_DESKTOP_IMAGE_INFO + { + public POINTL PathSourceSize; + public RECT DesktopImageRegion; + public RECT DesktopImageClip; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_VIDEO_SIGNAL_INFO + { + public ulong pixelRate; + public DISPLAYCONFIG_RATIONAL hSyncFreq; + public DISPLAYCONFIG_RATIONAL vSyncFreq; + public DISPLAYCONFIG_2DREGION activeSize; + public DISPLAYCONFIG_2DREGION totalSize; + public uint videoStandard; + public DISPLAYCONFIG_SCANLINE_ORDERING scanLineOrdering; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_TARGET_MODE + { + public DISPLAYCONFIG_VIDEO_SIGNAL_INFO targetVideoSignalInfo; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct DISPLAYCONFIG_MODE_INFO_union + { + [FieldOffset(0)] + public DISPLAYCONFIG_TARGET_MODE targetMode; + + [FieldOffset(0)] + public DISPLAYCONFIG_SOURCE_MODE sourceMode; + + [FieldOffset(0)] + public DISPLAYCONFIG_DESKTOP_IMAGE_INFO desktopImageInfo; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_PATH_SOURCE_INFO + { + public LUID adapterId; + public uint id; + public uint modeInfoIdx; + public DISPLAYCONFIG_SOURCE_FLAGS statusFlags; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_PATH_TARGET_INFO + { + public LUID adapterId; + public uint id; + public uint modeInfoIdx; + public DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY outputTechnology; + public DISPLAYCONFIG_ROTATION rotation; + public DISPLAYCONFIG_SCALING scaling; + public DISPLAYCONFIG_RATIONAL refreshRate; + public DISPLAYCONFIG_SCANLINE_ORDERING scanLineOrdering; + public bool targetAvailable; + public DISPLAYCONFIG_TARGET_FLAGS statusFlags; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_PATH_INFO + { + public DISPLAYCONFIG_PATH_SOURCE_INFO sourceInfo; + public DISPLAYCONFIG_PATH_TARGET_INFO targetInfo; + public DISPLAYCONFIG_PATH flags; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct DISPLAYCONFIG_MODE_INFO + { + public DISPLAYCONFIG_MODE_INFO_TYPE infoType; + public uint id; + public LUID adapterId; + public DISPLAYCONFIG_MODE_INFO_union info; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct DISPLAYCONFIG_SOURCE_DEVICE_NAME + { + public DISPLAYCONFIG_DEVICE_INFO_HEADER header; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string viewGdiDeviceName; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct DISPLAYCONFIG_TARGET_DEVICE_NAME_FLAGS + { + public uint value; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct DISPLAYCONFIG_TARGET_DEVICE_NAME + { + public DISPLAYCONFIG_DEVICE_INFO_HEADER header; + public DISPLAYCONFIG_TARGET_DEVICE_NAME_FLAGS flags; + public DISPLAYCONFIG_VIDEO_OUTPUT_TECHNOLOGY outputTechnology; + public ushort edidManufactureId; + public ushort edidProductCodeId; + public uint connectorInstance; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string monitorFriendlyDeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string monitorDevicePath; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } +} \ No newline at end of file diff --git a/novideo_srgb/DoubleToneCurve.cs b/novideo_srgb/DoubleToneCurve.cs new file mode 100644 index 0000000..5fb1160 --- /dev/null +++ b/novideo_srgb/DoubleToneCurve.cs @@ -0,0 +1,43 @@ +namespace novideo_srgb +{ + public class DoubleToneCurve : ToneCurve + { + private double[] _values; + + public DoubleToneCurve(double[] values) + { + _values = values; + } + + public double SampleAt(double x) + { + if (x == 0) return _values[0]; + if (x >= 1) return _values[_values.Length - 1]; + + var index = x * (_values.Length - 1); + var frac = index - (uint)index; + return _values[(uint)index] * (1 - frac) + _values[(uint)index + 1] * frac; + } + + public double SampleInverseAt(double x) + { + if (_values[0] >= x) return 0; + if (_values[_values.Length - 1] <= x) return 1; + + var lowerIndex = -1; + for (var i = 0; i < _values.Length; i++) + { + if (x >= _values[i]) + { + lowerIndex = i; + } + } + + var low = _values[lowerIndex]; + var high = _values[lowerIndex + 1]; + var frac = (x - low) / (high - low); + + return (lowerIndex + frac) / (_values.Length - 1); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/GammaToneCurve.cs b/novideo_srgb/GammaToneCurve.cs new file mode 100644 index 0000000..40e8540 --- /dev/null +++ b/novideo_srgb/GammaToneCurve.cs @@ -0,0 +1,97 @@ +using System; + +namespace novideo_srgb +{ + public class GammaToneCurve : ToneCurve + { + private readonly double _gamma; + private double _a = 1; + private double _b; + private readonly double _c; + + public unsafe GammaToneCurve(double gamma, double black = 0, double outputOffset = 1, bool relative = false) + { + if (black == 0) + { + _gamma = gamma; + return; + } + + if (outputOffset == 1) + { + _gamma = !relative + ? gamma + : Math.Log((black - 1) * Math.Pow(2, gamma) / (black * Math.Pow(2, gamma) - 1), 2); + _a = 1 - black; + _c = black; + } + else + { + var outBlack = outputOffset * black; + var btWhite = 1 - outBlack; + var btBlack = black - outBlack; + _c = outBlack; + + if (!relative) + { + _gamma = gamma; + CalculateBT1886(btWhite, btBlack); + } + else + { + // assume sane values for black and gamma + double lowD = 1; + double highD = 8; + + // what the hell + var low = *(ulong*)&lowD; + var high = *(ulong*)&highD; + + var target = Math.Pow(0.5, gamma); + + while (true) + { + var mid = (low + high) / 2; + _gamma = *(double*)∣ + CalculateBT1886(btWhite, btBlack); + var sample = SampleAt(0.5); + if (sample == target || low == mid || high == mid) + { + break; + } + + if (sample > target) + { + low = mid; + } + else + { + high = mid; + } + } + } + } + } + + private void CalculateBT1886(double white, double black) + { + var lwg = Math.Pow(white, 1 / _gamma); + var lbg = Math.Pow(black, 1 / _gamma); + _a = Math.Pow(lwg - lbg, _gamma); + _b = lbg / (lwg - lbg); + } + + public double SampleAt(double x) + { + if (x >= 1) return 1; + return _a * Math.Pow(Math.Max(x + _b, 0), _gamma) + _c; + } + + public double SampleInverseAt(double x) + { + if (_a != 1) throw new NotSupportedException(); + if (x >= 1) return 1; + return Math.Pow(x, 1 / _gamma); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/ICCBinaryReader.cs b/novideo_srgb/ICCBinaryReader.cs new file mode 100644 index 0000000..f688746 --- /dev/null +++ b/novideo_srgb/ICCBinaryReader.cs @@ -0,0 +1,57 @@ +using System.IO; +using System.Net; + +namespace novideo_srgb +{ + public class ICCBinaryReader : BinaryReader + { + public ICCBinaryReader(Stream stream) : base(stream) + { + } + + public override short ReadInt16() + { + return IPAddress.NetworkToHostOrder(base.ReadInt16()); + } + + public override ushort ReadUInt16() + { + return (ushort)ReadInt16(); + } + + public override int ReadInt32() + { + return IPAddress.NetworkToHostOrder(base.ReadInt32()); + } + + public override uint ReadUInt32() + { + return (uint)ReadInt32(); + } + + public override long ReadInt64() + { + return IPAddress.NetworkToHostOrder(base.ReadInt64()); + } + + public override ulong ReadUInt64() + { + return (ulong)ReadInt64(); + } + + public float ReadU8Fixed8() + { + return ReadUInt16() / 256f; + } + + public double ReadS15Fixed16() + { + return ReadInt32() / 65536d; + } + + public double ReadCIEXYZ() + { + return ReadUInt16() / 32768d; + } + } +} \ No newline at end of file diff --git a/novideo_srgb/ICCMatrixProfile.cs b/novideo_srgb/ICCMatrixProfile.cs new file mode 100644 index 0000000..18c65b7 --- /dev/null +++ b/novideo_srgb/ICCMatrixProfile.cs @@ -0,0 +1,287 @@ +using System; +using System.IO; + +namespace novideo_srgb +{ + public class ICCMatrixProfile + { + public Matrix matrix = Matrix.Zero3x3(); + public ToneCurve[] trcs = new ToneCurve[3]; + public ToneCurve[] vcgt; + + private ICCMatrixProfile() + { + } + + public static ICCMatrixProfile FromFile(string path) + { + var result = new ICCMatrixProfile(); + + using (var reader = new ICCBinaryReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))) + { + var stream = reader.BaseStream; + + { + stream.Seek(0x24, SeekOrigin.Begin); + var magic = new string(reader.ReadChars(4)); + if (magic != "acsp") + { + throw new ICCProfileException("Not an ICC profile"); + } + } + + { + stream.Seek(0xC, SeekOrigin.Begin); + var type = new string(reader.ReadChars(4)); + if (type != "mntr") + { + throw new ICCProfileException("Not a display device profile"); + } + } + + { + stream.Seek(0x10, SeekOrigin.Begin); + var spaces = new string(reader.ReadChars(8)); + if (spaces != "RGB XYZ ") + { + throw new ICCProfileException("Not an RGB profile with XYZ PCS"); + } + } + + stream.Seek(0x80, SeekOrigin.Begin); + + var tagCount = reader.ReadUInt32(); + + var seenTags = 0; + + var useCLUT = false; + for (uint i = 0; i < tagCount; i++) + { + stream.Seek(0x80 + 4 + 12 * i, SeekOrigin.Begin); + var tagSig = new string(reader.ReadChars(4)); + + var offset = reader.ReadUInt32(); + var size = reader.ReadUInt32(); + + reader.BaseStream.Seek(offset, SeekOrigin.Begin); + + var index = Array.IndexOf(new[] { 'r', 'g', 'b' }, tagSig[0]); + + if (tagSig == "A2B1") + { + useCLUT = true; + var typeSig = new string(reader.ReadChars(4)); + if (typeSig != "mft2") + { + throw new ICCProfileException(tagSig + " is not of lut16Type"); + } + + reader.ReadUInt32(); + + var inputChannels = reader.ReadByte(); + if (inputChannels != 3) + { + throw new ICCProfileException(tagSig + " must have 3 input channels"); + } + + var outputChannels = reader.ReadByte(); + if (outputChannels != 3) + { + throw new ICCProfileException(tagSig + " must have 3 output channels"); + } + + var lutPoints = reader.ReadByte(); + + reader.ReadByte(); + for (var j = 0; j < 9; j++) + { + reader.ReadS15Fixed16(); + } + + var inputEntries = reader.ReadUInt16(); + var outputEntries = reader.ReadUInt16(); + + var input = new ToneCurve[3]; + for (var j = 0; j < 3; j++) + { + var table = new ushort[inputEntries]; + for (var k = 0; k < inputEntries; k++) + { + table[k] = reader.ReadUInt16(); + } + + input[j] = new LutToneCurve(table); + } + + var clut = new ushort[lutPoints, lutPoints, lutPoints, 3]; + for (var r = 0; r < lutPoints; r++) + { + for (var g = 0; g < lutPoints; g++) + { + for (var b = 0; b < lutPoints; b++) + { + for (var j = 0; j < 3; j++) + { + clut[r, g, b, j] = reader.ReadUInt16(); + } + } + } + } + + var output = new LutToneCurve[3]; + for (var j = 0; j < 3; j++) + { + var table = new ushort[outputEntries]; + for (var k = 0; k < outputEntries; k++) + { + table[k] = reader.ReadUInt16(); + } + + output[j] = new LutToneCurve(table, 32768); + } + + var lut16 = new Lut16(input, clut, output); + var black = lut16.SampleGrayscaleAt(0); + + var primaries = new[] + { + lut16.SampleAt(1, 0, 0), + lut16.SampleAt(0, 1, 0), + lut16.SampleAt(0, 0, 1) + }; + + var Mprime = Matrix.Zero3x3(); + for (var j = 0; j < 3; j++) + { + var purePrimary = primaries[j] - black; + for (var k = 0; k < 3; k++) + { + Mprime[k, j] = purePrimary[k] / purePrimary[1]; + } + } + + var M = Mprime * Matrix.FromDiagonal(Mprime.Inverse() * Colorimetry.D50); + var Minv = M.Inverse(); + result.matrix = M; + + const int trcSize = 4096; + var trcs = new double[3][]; + for (var j = 0; j < 3; j++) + { + trcs[j] = new double[trcSize]; + } + + for (var j = 0; j < trcSize - 1; j++) + { + var values = lut16.SampleGrayscaleAt(j / (double)(trcSize - 1)); + + var toneResponse = Minv * values; + for (var k = 0; k < 3; k++) + { + trcs[k][j] = Math.Min(Math.Max(toneResponse[k], 0), 1); + } + } + + for (var j = 0; j < 3; j++) + { + trcs[j][trcSize - 1] = 1; + result.trcs[j] = new DoubleToneCurve(trcs[j]); + } + } + else if (tagSig.EndsWith("TRC") && !useCLUT) + { + var typeSig = new string(reader.ReadChars(4)); + if (typeSig != "curv") + { + throw new ICCProfileException(tagSig + " is not of curveType"); + } + + reader.ReadUInt32(); + + var numEntries = reader.ReadUInt32(); + + ToneCurve curve; + if (numEntries == 1) + { + var gamma = reader.ReadU8Fixed8(); + curve = new GammaToneCurve(gamma); + } + else + { + var entries = new ushort[numEntries]; + for (uint j = 0; j < numEntries; j++) + { + entries[j] = reader.ReadUInt16(); + } + + curve = new LutToneCurve(entries); + } + + result.trcs[index] = curve; + + seenTags++; + } + else if (tagSig.EndsWith("XYZ") && !useCLUT) + { + reader.ReadUInt32(); + reader.ReadUInt32(); + + for (var j = 0; j < 3; j++) + { + result.matrix[j, index] = reader.ReadS15Fixed16(); + } + + seenTags++; + } + else if (tagSig == "vcgt") + { + reader.ReadChars(4); + reader.ReadUInt32(); + var type = reader.ReadUInt32(); + if (type != 0) throw new ICCProfileException("Only VCGT type 0 is supported"); + + var numChannels = reader.ReadUInt16(); + var numEntries = reader.ReadUInt16(); + var entrySize = reader.ReadUInt16(); + + if (numChannels != 3) throw new ICCProfileException("Only VCGT with 3 channels is supported"); + + result.vcgt = new ToneCurve[3]; + for (var j = 0; j < 3; j++) + { + var values = new ushort[numEntries]; + for (var k = 0; k < numEntries; k++) + { + switch (entrySize) + { + case 1: + values[k] = (ushort)(reader.ReadByte() * ushort.MaxValue / byte.MaxValue); + break; + case 2: + values[k] = reader.ReadUInt16(); + break; + default: + throw new ICCProfileException("Only 8 and 16 bit VCGT is supported"); + } + } + + result.vcgt[j] = new LutToneCurve(values); + } + } + } + + if (!useCLUT) + { + if (seenTags != 6) + { + throw new ICCProfileException("Missing required tags for curves + matrix profile"); + } + + result.matrix = Colorimetry.XYZScaleToD50(result.matrix); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/novideo_srgb/ICCProfileException.cs b/novideo_srgb/ICCProfileException.cs new file mode 100644 index 0000000..7ae0d2a --- /dev/null +++ b/novideo_srgb/ICCProfileException.cs @@ -0,0 +1,11 @@ +using System; + +namespace novideo_srgb +{ + public class ICCProfileException : FormatException + { + public ICCProfileException(string message) : base(message) + { + } + } +} \ No newline at end of file diff --git a/novideo_srgb/Lut16.cs b/novideo_srgb/Lut16.cs new file mode 100644 index 0000000..ca039cd --- /dev/null +++ b/novideo_srgb/Lut16.cs @@ -0,0 +1,123 @@ +using System; + +namespace novideo_srgb +{ + public class Lut16 + { + private ushort[,,,] _lut; + private int _lutSize; + + private ToneCurve[] inputCurves; + private ToneCurve[] outputCurves; + + public Lut16(ToneCurve[] input, ushort[,,,] lut, ToneCurve[] output) + { + _lut = lut; + _lutSize = _lut.GetLength(0); + + inputCurves = input; + outputCurves = output; + } + + public Matrix SampleGrayscaleAt(double index) + { + return SampleAt(index, index, index); + } + + public Matrix SampleAt(double r, double g, double b) + { + var result = Matrix.FromValues(new[,] + { + { inputCurves[0].SampleAt(r) }, + { inputCurves[1].SampleAt(g) }, + { inputCurves[2].SampleAt(b) } + }); + + result = SampleCLUTTetrahedral(result); + + for (var i = 0; i < 3; i++) + { + result[i] = outputCurves[i].SampleAt(result[i]); + } + + return result; + } + + private Matrix S(int x, int y, int z) + { + x = Math.Min(x, _lutSize - 1); + y = Math.Min(y, _lutSize - 1); + z = Math.Min(z, _lutSize - 1); + var sample0 = _lut[x, y, z, 0] / (double)ushort.MaxValue; + var sample1 = _lut[x, y, z, 1] / (double)ushort.MaxValue; + var sample2 = _lut[x, y, z, 2] / (double)ushort.MaxValue; + + return Matrix.FromValues(new[,] { { sample0 }, { sample1 }, { sample2 } }); + } + + private Matrix S(double x, double y, double z) + { + return S((int)x, (int)y, (int)z); + } + + // https://www.filmlight.ltd.uk/pdf/whitepapers/FL-TL-TN-0057-SoftwareLib.pdf + private Matrix SampleCLUTTetrahedral(Matrix rgb) + { + var lutIndex = rgb * (_lutSize - 1); + var n = lutIndex.Map(Math.Floor); + var f = lutIndex.Map(x => x - (int)x); + + Matrix Sxyz; + if (f[0] > f[1]) + { + if (f[1] > f[2]) + { + Sxyz = (1 - f[0]) * S(n[0], n[1], n[2]) + + (f[0] - f[1]) * S(n[0] + 1, n[1], n[2]) + + (f[1] - f[2]) * S(n[0] + 1, n[1] + 1, n[2]) + + (f[2]) * S(n[0] + 1, n[1] + 1, n[2] + 1); + } + else if (f[0] > f[2]) + { + Sxyz = (1 - f[0]) * S(n[0], n[1], n[2]) + + (f[0] - f[2]) * S(n[0] + 1, n[1], n[2]) + + (f[2] - f[1]) * S(n[0] + 1, n[1], n[2] + 1) + + (f[1]) * S(n[0] + 1, n[1] + 1, n[2] + 1); + } + else + { + Sxyz = (1 - f[2]) * S(n[0], n[1], n[2]) + + (f[2] - f[0]) * S(n[0], n[1], n[2] + 1) + + (f[0] - f[1]) * S(n[0] + 1, n[1], n[2] + 1) + + (f[1]) * S(n[0] + 1, n[1] + 1, n[2] + 1); + } + } + else + { + if (f[2] > f[1]) + { + Sxyz = (1 - f[2]) * S(n[0], n[1], n[2]) + + (f[2] - f[1]) * S(n[0], n[1], n[2] + 1) + + (f[1] - f[0]) * S(n[0], n[1] + 1, n[2] + 1) + + (f[0]) * S(n[0] + 1, n[1] + 1, n[2] + 1); + } + else if (f[2] > f[0]) + { + Sxyz = (1 - f[1]) * S(n[0], n[1], n[2]) + + (f[1] - f[2]) * S(n[0], n[1] + 1, n[2]) + + (f[2] - f[0]) * S(n[0], n[1] + 1, n[2] + 1) + + (f[0]) * S(n[0] + 1, n[1] + 1, n[2] + 1); + } + else + { + Sxyz = (1 - f[1]) * S(n[0], n[1], n[2]) + + (f[1] - f[0]) * S(n[0], n[1] + 1, n[2]) + + (f[0] - f[2]) * S(n[0] + 1, n[1] + 1, n[2]) + + (f[2]) * S(n[0] + 1, n[1] + 1, n[2] + 1); + } + } + + return Sxyz; + } + } +} \ No newline at end of file diff --git a/novideo_srgb/LutToneCurve.cs b/novideo_srgb/LutToneCurve.cs new file mode 100644 index 0000000..3db1f11 --- /dev/null +++ b/novideo_srgb/LutToneCurve.cs @@ -0,0 +1,48 @@ +namespace novideo_srgb +{ + public class LutToneCurve : ToneCurve + { + private ushort[] _values; + private ushort _divisor; + + public LutToneCurve(ushort[] values, ushort divisor = ushort.MaxValue) + { + _values = values; + _divisor = divisor; + } + + public double SampleAt(double x) + { + if (x == 0) return (double)_values[0] / _divisor; + if (x >= 1) return (double)_values[_values.Length - 1] / _divisor; + + var index = x * (_values.Length - 1); + var frac = index - (uint)index; + return (_values[(uint)index] * (1 - frac) + _values[(uint)index + 1] * frac) / _divisor; + } + + public double SampleInverseAt(double x) + { + var value = x * _divisor; + var lowValue = (ushort)value; + + if (_values[0] >= value) return 0; + if (_values[_values.Length - 1] <= lowValue) return 1; + + var lowerIndex = -1; + for (var i = 0; i < _values.Length; i++) + { + if (lowValue >= _values[i]) + { + lowerIndex = i; + } + } + + var low = _values[lowerIndex]; + var high = _values[lowerIndex + 1]; + var frac = (value - low) / (high - low); + + return (lowerIndex + frac) / (_values.Length - 1); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/MainViewModel.cs b/novideo_srgb/MainViewModel.cs new file mode 100644 index 0000000..aa1bbe1 --- /dev/null +++ b/novideo_srgb/MainViewModel.cs @@ -0,0 +1,109 @@ +using Microsoft.Win32; +using NvAPIWrapper.Display; +using System.Collections.ObjectModel; +using System.IO; +using System.Xml.Linq; + +namespace novideo_srgb +{ + public class MainViewModel + { + public static string ConfigPath { get; set; } + + public ObservableCollection Monitors { get; } + + private string _configPath; + + public MainViewModel() + { + Monitors = new ObservableCollection(); + _configPath = Path.Combine(ConfigPath, "NovideoConfig.xml"); + + UpdateMonitors(); + } + + private void UpdateMonitors() + { + Monitors.Clear(); + List config = null; + if (File.Exists(_configPath)) + { + config = XElement.Load(_configPath).Descendants("monitor").ToList(); + } + + var hdrPaths = DisplayConfigManager.GetHdrDisplayPaths(); + + var number = 1; + foreach (var display in Display.GetDisplays()) + { + var displays = WindowsDisplayAPI.Display.GetDisplays(); + var path = displays.First(x => x.DisplayName == display.Name).DevicePath; + + var hdrActive = hdrPaths.Contains(path); + + var settings = config?.FirstOrDefault(x => (string)x.Attribute("path") == path); + MonitorData monitor; + if (settings != null) + { + monitor = new MonitorData(this, number++, display, path, hdrActive, + (bool)settings.Attribute("clamp_sdr"), + (bool)settings.Attribute("use_icc"), + (string)settings.Attribute("icc_path"), + (bool)settings.Attribute("calibrate_gamma"), + (int)settings.Attribute("selected_gamma"), + (double)settings.Attribute("custom_gamma"), + (double)settings.Attribute("custom_percentage"), + (int)settings.Attribute("target"), + (bool)settings.Attribute("disable_optimization")); + } + else + { + monitor = new MonitorData(this, number++, display, path, hdrActive, false); + } + + Monitors.Add(monitor); + } + + foreach (var monitor in Monitors) + { + monitor.ReapplyClamp(); + } + } + + public void OnDisplaySettingsChanged(object sender, EventArgs e) + { + UpdateMonitors(); + } + + public void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) + { + if (e.Mode != PowerModes.Resume) return; + OnDisplaySettingsChanged(null, null); + } + + public void SaveConfig() + { + try + { + var xElem = new XElement("monitors", + Monitors.Select(x => + new XElement("monitor", new XAttribute("path", x.Path), + new XAttribute("clamp_sdr", x.ClampSdr), + new XAttribute("use_icc", x.UseIcc), + new XAttribute("icc_path", x.ProfilePath), + new XAttribute("calibrate_gamma", x.CalibrateGamma), + new XAttribute("selected_gamma", x.SelectedGamma), + new XAttribute("custom_gamma", x.CustomGamma), + new XAttribute("custom_percentage", x.CustomPercentage), + new XAttribute("target", x.Target), + new XAttribute("disable_optimization", x.DisableOptimization)))); + xElem.Save(_configPath); + } + catch (Exception ex) + { + MessageBox.Show(ex.Message + "\n\nTry extracting the program elsewhere."); + Environment.Exit(1); + } + } + } +} \ No newline at end of file diff --git a/novideo_srgb/MainWindow.xaml b/novideo_srgb/MainWindow.xaml new file mode 100644 index 0000000..d200f80 --- /dev/null +++ b/novideo_srgb/MainWindow.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/novideo_srgb/MainWindow.xaml.cs b/novideo_srgb/MainWindow.xaml.cs new file mode 100644 index 0000000..412788c --- /dev/null +++ b/novideo_srgb/MainWindow.xaml.cs @@ -0,0 +1,136 @@ +using Microsoft.Win32; +using System.ComponentModel; +using System.Windows; + +namespace novideo_srgb +{ + public partial class MainWindow + { + private readonly MainViewModel _viewModel; + + private static MainWindow _novideoWindow; + + public MainWindow() + { + InitializeComponent(); + _viewModel = (MainViewModel)DataContext; + SystemEvents.DisplaySettingsChanged += _viewModel.OnDisplaySettingsChanged; + SystemEvents.PowerModeChanged += _viewModel.OnPowerModeChanged; + } + + public static void CreateAndShow(bool show = true) + { + if (System.Windows.Application.Current == null) + { + new System.Windows.Application(); + } + + _novideoWindow ??= new MainWindow(); + + if (show) + { + _novideoWindow.WindowState = WindowState.Normal; + _novideoWindow.Show(); + _novideoWindow.Topmost = true; + _novideoWindow.Topmost = false; + } + } + + public static bool IsInitialized() + { + return _novideoWindow != null; + } + + public static void UpdateContextMenu(ToolStripMenuItem parent) + { + if (_novideoWindow == null) + { + return; + } + + var menu = new ToolStripDropDownMenu(); + + parent.DropDownItems.Clear(); + + foreach (var monitor in _novideoWindow._viewModel.Monitors) + { + var item = new ToolStripMenuItem(); + parent.DropDownItems.Add(item); + item.Text = monitor.Name; + item.Checked = monitor.Clamped; + item.Enabled = monitor.CanClamp; + item.ForeColor = parent.ForeColor; + item.Click += (sender, args) => monitor.Clamped = !monitor.Clamped; + } + + parent.DropDownItems.Add("-").ForeColor = parent.ForeColor; + + var reapplyItem = new ToolStripMenuItem(); + parent.DropDownItems.Add(reapplyItem); + reapplyItem.Text = "Reapply"; + reapplyItem.ForeColor = parent.ForeColor; + reapplyItem.Click += delegate { _novideoWindow.ReapplyMonitorSettings(); }; + } + + protected override void OnClosing(CancelEventArgs e) + { + e.Cancel = true; + + Hide(); + } + + protected override void OnStateChanged(EventArgs e) + { + if (WindowState == WindowState.Minimized) + { + Hide(); + } + + base.OnStateChanged(e); + } + + private void AboutButton_Click(object sender, RoutedEventArgs o) + { + var window = new AboutWindow + { + Owner = this + }; + window.ShowDialog(); + } + + private void AdvancedButton_Click(object sender, RoutedEventArgs e) + { + if (System.Windows.Application.Current.Windows.Cast().Any(x => x is AdvancedWindow)) return; + var monitor = ((FrameworkElement)sender).DataContext as MonitorData; + var window = new AdvancedWindow(monitor) + { + Owner = this + }; + + void CloseWindow(object o, EventArgs e2) => window.Close(); + + SystemEvents.DisplaySettingsChanged += CloseWindow; + if (window.ShowDialog() == false) return; + SystemEvents.DisplaySettingsChanged -= CloseWindow; + + if (window.ChangedCalibration) + { + _viewModel.SaveConfig(); + monitor?.ReapplyClamp(); + } + } + + private void ReapplyButton_Click(object sender, RoutedEventArgs e) + { + ReapplyMonitorSettings(); + } + + private void ReapplyMonitorSettings() + { + foreach (var monitor in _viewModel.Monitors) + { + monitor.ReapplyClamp(); + } + } + } +} \ No newline at end of file diff --git a/novideo_srgb/Matrix.cs b/novideo_srgb/Matrix.cs new file mode 100644 index 0000000..8354688 --- /dev/null +++ b/novideo_srgb/Matrix.cs @@ -0,0 +1,235 @@ +using System; + +namespace novideo_srgb +{ + public class Matrix + { + private double[,] _values; + + private Matrix() + { + } + + public double this[int x, int y] + { + get => _values[x, y]; + set => _values[x, y] = value; + } + + public double this[int x] + { + get + { + if (Cols != 1) + { + throw new NotSupportedException("Matrix must be 3x1"); + } + + return _values[x, 0]; + } + set + { + if (Cols != 1) + { + throw new NotSupportedException("Matrix must be 3x1"); + } + + _values[x, 0] = value; + } + } + + public int Rows => _values.GetLength(0); + public int Cols => _values.GetLength(1); + + public static Matrix FromValues(double[,] array) + { + if (array.GetLength(0) != 3 || !(array.GetLength(1) == 3 || array.GetLength(1) == 1)) + { + throw new ArgumentException("Array must be 3x3 or 3x1"); + } + + var result = new Matrix + { + _values = array + }; + return result; + } + + public static Matrix Zero3x3() + { + return FromValues(new double[3, 3]); + } + + public static Matrix Zero3x1() + { + return FromValues(new double[3, 1]); + } + + public static Matrix One3x1() + { + return FromValues(new double[,] { { 1 }, { 1 }, { 1 } }); + } + + public static Matrix FromDiagonal(double[] array) + { + if (array.Length != 3) + { + throw new ArgumentException("Array must have length 3"); + } + + var result = new Matrix + { + _values = new double[3, 3] + }; + for (var i = 0; i < 3; i++) + { + result._values[i, i] = array[i]; + } + + return result; + } + + public static Matrix FromDiagonal(Matrix column) + { + if (column.Cols != 1) + { + throw new ArgumentException("Matrix must be 3x1"); + } + + return FromDiagonal(new[] { column[0], column[1], column[2] }); + } + + public static Matrix operator *(Matrix a, Matrix b) + { + if (a.Cols != 3) + { + throw new ArgumentException("Left side must be 3x3"); + } + + var result = b.Cols == 3 ? Zero3x3() : Zero3x1(); + + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < result.Cols; j++) + { + for (var k = 0; k < 3; k++) + { + result[i, j] += a[i, k] * b[k, j]; + } + } + } + + return result; + } + + public static Matrix operator *(double a, Matrix b) + { + var result = b.Cols == 1 ? Zero3x1() : Zero3x3(); + + for (var i = 0; i < result.Rows; i++) + { + for (var j = 0; j < result.Cols; j++) + { + result[i, j] = a * b[i, j]; + } + } + + return result; + } + + public static Matrix operator *(Matrix a, double b) + { + return b * a; + } + + public static Matrix operator /(double a, Matrix b) + { + return 1 / a * b; + } + + public static Matrix operator /(Matrix a, double b) + { + return a * (1 / b); + } + + public static Matrix operator +(Matrix a, Matrix b) + { + if (a.Cols != b.Cols) + { + throw new ArgumentException("Both sides must have same size"); + } + + var result = b.Cols == 1 ? Zero3x1() : Zero3x3(); + + for (var i = 0; i < result.Rows; i++) + { + for (var j = 0; j < result.Cols; j++) + { + result[i, j] = a[i, j] + b[i, j]; + } + } + + return result; + } + + public static Matrix operator -(Matrix a, Matrix b) + { + if (a.Cols != b.Cols) + { + throw new ArgumentException("Both sides must have same size"); + } + + var result = b.Cols == 1 ? Zero3x1() : Zero3x3(); + + for (var i = 0; i < result.Rows; i++) + { + for (var j = 0; j < result.Cols; j++) + { + result[i, j] = a[i, j] - b[i, j]; + } + } + + return result; + } + + public Matrix Inverse() + { + if (Cols != 3) + { + throw new ArgumentException("Matrix must be 3x3"); + } + + var a = this[0, 0]; + var b = this[0, 1]; + var c = this[0, 2]; + var d = this[1, 0]; + var e = this[1, 1]; + var f = this[1, 2]; + var g = this[2, 0]; + var h = this[2, 1]; + var i = this[2, 2]; + + var denom = a * e * i - a * f * h - b * d * i + b * f * g + c * d * h - c * e * g; + return 1 / denom * FromValues(new[,] + { + { e * i - f * h, -b * i + c * h, b * f - c * e }, + { -d * i + f * g, a * i - c * g, -a * f + c * d }, + { d * h - e * g, -a * h + b * g, a * e - b * d } + }); + } + + public Matrix Map(Func func) + { + var result = Cols == 1 ? Zero3x1() : Zero3x3(); + for (var i = 0; i < result.Rows; i++) + { + for (var j = 0; j < result.Cols; j++) + { + result[i, j] = func(this[i, j]); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/novideo_srgb/MonitorData.cs b/novideo_srgb/MonitorData.cs new file mode 100644 index 0000000..12eb1de --- /dev/null +++ b/novideo_srgb/MonitorData.cs @@ -0,0 +1,212 @@ +using EDIDParser; +using EDIDParser.Descriptors; +using EDIDParser.Enums; +using NLog; +using NvAPIWrapper.Display; +using NvAPIWrapper.GPU; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace novideo_srgb +{ + public class MonitorData : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + private readonly GPUOutput _output; + private bool _clamped; + + private MainViewModel _viewModel; + + public MonitorData(MainViewModel viewModel, int number, Display display, string path, bool hdrActive, bool clampSdr) + { + _viewModel = viewModel; + Number = number; + _output = display.Output; + + Edid = Novideo.GetEDID(path, display); + + Name = Edid.Descriptors.OfType() + .FirstOrDefault(x => x.Type == StringDescriptorType.MonitorName)?.Value ?? ""; + + Path = path; + ClampSdr = clampSdr; + HdrActive = hdrActive; + + var coords = Edid.DisplayParameters.ChromaticityCoordinates; + EdidColorSpace = new Colorimetry.ColorSpace + { + Red = new Colorimetry.Point { X = Math.Round(coords.RedX, 3), Y = Math.Round(coords.RedY, 3) }, + Green = new Colorimetry.Point { X = Math.Round(coords.GreenX, 3), Y = Math.Round(coords.GreenY, 3) }, + Blue = new Colorimetry.Point { X = Math.Round(coords.BlueX, 3), Y = Math.Round(coords.BlueY, 3) }, + White = Colorimetry.D65 + }; + + _clamped = Novideo.IsColorSpaceConversionActive(_output); + + ProfilePath = ""; + CustomGamma = 2.2; + CustomPercentage = 100; + } + + public MonitorData(MainViewModel viewModel, int number, Display display, string path, bool hdrActive, bool clampSdr, bool useIcc, string profilePath, + bool calibrateGamma, + int selectedGamma, double customGamma, double customPercentage, int target, bool disableOptimization) : + this(viewModel, number, display, path, hdrActive, clampSdr) + { + UseIcc = useIcc; + ProfilePath = profilePath; + CalibrateGamma = calibrateGamma; + SelectedGamma = selectedGamma; + CustomGamma = customGamma; + CustomPercentage = customPercentage; + Target = target; + DisableOptimization = disableOptimization; + } + + public int Number { get; } + public string Name { get; } + public EDID Edid { get; } + public string Path { get; } + public bool ClampSdr { get; set; } + public bool HdrActive { get; } + + private void UpdateClamp(bool doClamp) + { + if (_clamped) + { + Novideo.DisableColorSpaceConversion(_output); + } + + if (!doClamp) return; + + if (_clamped) Thread.Sleep(100); + if (UseEdid) + Novideo.SetColorSpaceConversion(_output, Colorimetry.RGBToRGB(TargetColorSpace, EdidColorSpace)); + else if (UseIcc) + { + var profile = ICCMatrixProfile.FromFile(ProfilePath); + if (CalibrateGamma) + { + var trcBlack = Matrix.FromValues(new[,] + { + { profile.trcs[0].SampleAt(0) }, + { profile.trcs[1].SampleAt(0) }, + { profile.trcs[2].SampleAt(0) } + }); + var black = (profile.matrix * trcBlack)[1]; + + ToneCurve gamma; + switch (SelectedGamma) + { + case 0: + gamma = new SrgbEOTF(black); + break; + case 1: + gamma = new GammaToneCurve(2.4, black, 0); + break; + case 2: + gamma = new GammaToneCurve(CustomGamma, black, CustomPercentage / 100); + break; + case 3: + gamma = new GammaToneCurve(CustomGamma, black, CustomPercentage / 100, true); + break; + default: + throw new NotSupportedException("Unsupported gamma type " + SelectedGamma); + } + + Novideo.SetColorSpaceConversion(_output, profile, TargetColorSpace, gamma, DisableOptimization); + } + else + { + Novideo.SetColorSpaceConversion(_output, profile, TargetColorSpace); + } + } + } + + private void HandleClampException(Exception e) + { + Logger.Error(e); + + //System.Windows.MessageBox.Show(e.Message); + _clamped = Novideo.IsColorSpaceConversionActive(_output); + ClampSdr = _clamped; + _viewModel.SaveConfig(); + OnPropertyChanged(nameof(Clamped)); + } + + public bool Clamped + { + set + { + try + { + UpdateClamp(value); + ClampSdr = value; + _viewModel.SaveConfig(); + } + catch (Exception e) + { + HandleClampException(e); + return; + } + + _clamped = value; + OnPropertyChanged(); + } + get => _clamped; + } + + public void ReapplyClamp() + { + try + { + var clamped = CanClamp && ClampSdr; + UpdateClamp(clamped); + _clamped = clamped; + OnPropertyChanged(nameof(CanClamp)); + } + catch (Exception e) + { + HandleClampException(e); + } + } + + public bool CanClamp => !HdrActive && (UseEdid && !EdidColorSpace.Equals(TargetColorSpace) || UseIcc && ProfilePath != ""); + + public string GPU => _output.PhysicalGPU.FullName; + + public bool UseEdid + { + set => UseIcc = !value; + get => !UseIcc; + } + + public bool UseIcc { set; get; } + + public string ProfilePath { set; get; } + + public bool CalibrateGamma { set; get; } + + public int SelectedGamma { set; get; } + + public double CustomGamma { set; get; } + + public double CustomPercentage { set; get; } + + public bool DisableOptimization { set; get; } + + public int Target { set; get; } + + public Colorimetry.ColorSpace EdidColorSpace { get; } + + private Colorimetry.ColorSpace TargetColorSpace => Colorimetry.ColorSpaces[Target]; + + private void OnPropertyChanged([CallerMemberName] string name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/Novideo.cs b/novideo_srgb/Novideo.cs new file mode 100644 index 0000000..d116ef2 --- /dev/null +++ b/novideo_srgb/Novideo.cs @@ -0,0 +1,308 @@ +using EDIDParser; +using Microsoft.Win32; +using NvAPIWrapper.Display; +using NvAPIWrapper.GPU; +using System.Runtime.InteropServices; + +/* +no NDAs violated here! +everything figured out from publicly available information and/or +throwing stuff at NVAPI and observing the results or errors +*/ + +namespace novideo_srgb +{ + public static class Novideo + { + /* + observed pipeline: content degamma -> content to srgb -> srgb to monitor -> matrix2 -> matrix1 -> monitor gamma + in hardware all the matrix stuff is done with a single matrix, i.e. these four multiplied together + 3x4 matrices cannot be multiplied with each other though, so only the 3x3 parts are combined "properly" + and the offsets are simply added together + */ + [StructLayout(LayoutKind.Sequential)] + private struct Csc + { + public uint version; // 0x1007C for V1, 0x200A0 for V2 + + public uint + contentColorSpace; // built-in degamut/degamma transforms, 1 <= x <= 12, default 2 (probably srgb) + + public uint monitorColorSpace; // built-in gamut/gamma transforms, 0 <= x <= 12, default 0 (= csc disabled) + public uint unknown1; // no idea, set to 0 by both get and set functions -> some type of error code? + public uint unknown2; // also no idea, not modified by either function -> unused? + public uint useMatrix1; // 1 to enable + public unsafe fixed float matrix1[3 * 4]; // r/g/b gain and offset + public uint useMatrix2; + + public unsafe fixed float matrix2[3 * 4]; + + // v2 stuff + public unsafe float* degamma; // pointer to degamma part of buffer (= first element) + public unsafe float* regamma; // pointer to regamma part of buffer (= index 0x3000) + + public unsafe float* + buffer; // float array of size 0x6000, contains interleaved rgb degamma followed by regamma + + public int bufferSize; // 0x6000 + } + + private const uint _NvAPI_GPU_GetColorSpaceConversion = 0x8159E87A; + private const uint _NvAPI_GPU_SetColorSpaceConversion = 0x0FCABD23A; + + [DllImport("nvapi64", EntryPoint = "nvapi_QueryInterface")] + private static extern IntPtr NvAPI_QueryInterface(uint id); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int NvAPI_GPU_GetColorSpaceConversion_t(uint displayId, + [MarshalAs(UnmanagedType.Struct)] ref Csc csc); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate int NvAPI_GPU_SetColorSpaceConversion_t(uint displayId, + [MarshalAs(UnmanagedType.Struct)] ref Csc csc); + + private static NvAPI_GPU_GetColorSpaceConversion_t NvAPI_GPU_GetColorSpaceConversion; + private static NvAPI_GPU_SetColorSpaceConversion_t NvAPI_GPU_SetColorSpaceConversion; + + public struct ColorSpaceConversion + { + public uint contentColorSpace; + public uint monitorColorSpace; + public float[,] matrix1; + public float[,] matrix2; + } + + public static ColorSpaceConversion GetColorSpaceConversion(GPUOutput output) + { + var displayId = output.PhysicalGPU.GetDisplayDeviceByOutput(output).DisplayId; + + var csc = new Csc { version = 0x1007C }; + var status = NvAPI_GPU_GetColorSpaceConversion(displayId, ref csc); + if (status != 0) + { + throw new Exception("NvAPI_GPU_GetColorSpaceConversion failed with error code " + status); + } + + var result = new ColorSpaceConversion + { + contentColorSpace = csc.contentColorSpace, + monitorColorSpace = csc.monitorColorSpace + }; + + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 4; j++) + { + unsafe + { + if (csc.useMatrix1 == 1) + { + if (result.matrix1 == null) result.matrix1 = new float[3, 4]; + result.matrix1[i, j] = csc.matrix1[i * 4 + j]; + } + + if (csc.useMatrix2 == 1) + { + if (result.matrix2 == null) result.matrix2 = new float[3, 4]; + result.matrix2[i, j] = csc.matrix2[i * 4 + j]; + } + } + } + } + + return result; + } + + public static void SetColorSpaceConversion(GPUOutput output, ColorSpaceConversion conversion) + { + var displayId = output.PhysicalGPU.GetDisplayDeviceByOutput(output).DisplayId; + + var csc = new Csc + { + version = 0x1007C, + contentColorSpace = conversion.contentColorSpace, + monitorColorSpace = conversion.monitorColorSpace + }; + + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 4; j++) + { + unsafe + { + if (conversion.matrix1 != null) + { + csc.useMatrix1 = 1; + csc.matrix1[i * 4 + j] = conversion.matrix1[i, j]; + } + + if (conversion.matrix2 != null) + { + csc.useMatrix2 = 1; + csc.matrix2[i * 4 + j] = conversion.matrix2[i, j]; + } + } + } + } + + var status = NvAPI_GPU_SetColorSpaceConversion(displayId, ref csc); + if (status != 0) + { + throw new Exception("NvAPI_GPU_SetColorSpaceConversion failed with error code " + status); + } + } + + public static void SetColorSpaceConversion(GPUOutput output, Matrix matrix) + { + SetColorSpaceConversion(output, MatrixToColorSpaceConversion(matrix)); + } + + public static unsafe void SetColorSpaceConversion(GPUOutput output, ICCMatrixProfile profile, + Colorimetry.ColorSpace target, + ToneCurve curve = null, + bool disableOptimization = false) + { + var matrix = profile.matrix.Inverse() * Colorimetry.RGBToPCSXYZ(target); + + if (curve == null) + { + SetColorSpaceConversion(output, MatrixToColorSpaceConversion(matrix)); + return; + } + + var displayId = output.PhysicalGPU.GetDisplayDeviceByOutput(output).DisplayId; + var gamma = new float[2, 1024, 3]; + fixed (float* buffer = gamma) + { + var csc = new Csc + { + version = 0x200A0, + contentColorSpace = 2, + monitorColorSpace = 2, + degamma = buffer, + regamma = buffer + 0x3000 / sizeof(float), + buffer = buffer, + bufferSize = 0x6000, + }; + + double nextIndex = -1; + for (var i = 1; i < 1024; i++) + { + var index = i / 1023d; + + if (!disableOptimization) + { + var curr = i * 255 % 1023; + var next = (i + 1) * 255 % 1023; + + if (nextIndex != -1) + { + index = nextIndex; + nextIndex = -1; + } + else if (next < curr) + { + nextIndex = (i + 1) * 255 / 1023 / 255d; + if (next != 0) + { + index = nextIndex; + } + } + } + + var sample = (float)curve.SampleAt(index); + + for (var j = 0; j < 3; j++) + { + gamma[0, i, j] = sample; + } + } + + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 3; j++) + { + csc.matrix1[i * 4 + j] = (float)matrix[i, j]; + } + } + + csc.useMatrix1 = 1; + + for (var i = 0; i < 1024; i++) + { + for (var j = 0; j < 3; j++) + { + var value = profile.trcs[j].SampleInverseAt(i / 1023d); + + if (profile.vcgt != null) + { + value = profile.vcgt[j].SampleAt(value); + } + + gamma[1, i, j] = (float)value; + } + } + + var status = NvAPI_GPU_SetColorSpaceConversion(displayId, ref csc); + if (status != 0) + { + throw new Exception("NvAPI_GPU_SetColorSpaceConversion failed with error code " + status); + } + } + } + + public static bool IsColorSpaceConversionActive(GPUOutput output) + { + return GetColorSpaceConversion(output).monitorColorSpace != 0; + } + + public static void DisableColorSpaceConversion(GPUOutput output) + { + SetColorSpaceConversion(output, new ColorSpaceConversion { contentColorSpace = 2 }); + } + + public static EDID GetEDID(string path, Display display) + { + try + { + var registryPath = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Enum\\DISPLAY\\"; + registryPath += string.Join("\\", path.Split('#').Skip(1).Take(2)); + return new EDID((byte[])Registry.GetValue(registryPath + "\\Device Parameters", "EDID", null)); + } + catch + { + return new EDID(display.Output.PhysicalGPU.ReadEDIDData(display.Output)); + } + } + + private static ColorSpaceConversion MatrixToColorSpaceConversion(Matrix matrix) + { + var csc = new ColorSpaceConversion + { + contentColorSpace = 2, + monitorColorSpace = 2, + matrix1 = new float[3, 4] + }; + + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 3; j++) + { + csc.matrix1[i, j] = (float)matrix[i, j]; + } + } + + return csc; + } + + static Novideo() + { + NvAPI_GPU_GetColorSpaceConversion = + Marshal.GetDelegateForFunctionPointer( + NvAPI_QueryInterface(_NvAPI_GPU_GetColorSpaceConversion)); + NvAPI_GPU_SetColorSpaceConversion = + Marshal.GetDelegateForFunctionPointer( + NvAPI_QueryInterface(_NvAPI_GPU_SetColorSpaceConversion)); + } + } +} \ No newline at end of file diff --git a/novideo_srgb/Properties/Resources.Designer.cs b/novideo_srgb/Properties/Resources.Designer.cs new file mode 100644 index 0000000..2488f13 --- /dev/null +++ b/novideo_srgb/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace novideo_srgb.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("novideo_srgb.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon icon { + get { + object obj = ResourceManager.GetObject("icon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + } +} diff --git a/novideo_srgb/Properties/Resources.resx b/novideo_srgb/Properties/Resources.resx new file mode 100644 index 0000000..79af2d9 --- /dev/null +++ b/novideo_srgb/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/novideo_srgb/Properties/Settings.Designer.cs b/novideo_srgb/Properties/Settings.Designer.cs new file mode 100644 index 0000000..66bab65 --- /dev/null +++ b/novideo_srgb/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace novideo_srgb.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.5.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/novideo_srgb/Properties/Settings.settings b/novideo_srgb/Properties/Settings.settings new file mode 100644 index 0000000..564d334 --- /dev/null +++ b/novideo_srgb/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/novideo_srgb/RangeRule.cs b/novideo_srgb/RangeRule.cs new file mode 100644 index 0000000..aa26e7c --- /dev/null +++ b/novideo_srgb/RangeRule.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.Windows.Controls; +using static System.Double; + +namespace novideo_srgb +{ + public class RangeRule : ValidationRule + { + public int Min { get; set; } + public int Max { get; set; } + + public override ValidationResult Validate(object valueObj, CultureInfo cultureInfo) + { + if (valueObj == null) return null; + + var valueString = (string)valueObj; + if (valueString.EndsWith(".")) + { + return new ValidationResult(false, "Input must not end in '.'"); + } + + double value = 0; + try + { + if (valueString.Length > 0) + value = Parse(valueString.Replace(',', 'a'), CultureInfo.InvariantCulture); + } + catch (Exception e) + { + return new ValidationResult(false, $"Illegal characters or {e.Message}"); + } + + if (value < Min || value > Max) + { + return new ValidationResult(false, + $"Value must be between {Min} and {Max}"); + } + + return ValidationResult.ValidResult; + } + } +} \ No newline at end of file diff --git a/novideo_srgb/SrgbEOTF.cs b/novideo_srgb/SrgbEOTF.cs new file mode 100644 index 0000000..ca188fa --- /dev/null +++ b/novideo_srgb/SrgbEOTF.cs @@ -0,0 +1,40 @@ +using System; + +namespace novideo_srgb +{ + public class SrgbEOTF : ToneCurve + { + private double _black; + + public SrgbEOTF(double black) + { + _black = black; + } + + public double SampleAt(double x) + { + if (x >= 1) return 1; + + double result; + if (x <= 0.04045) + { + result = x / 12.92; + } + else + { + result = Math.Pow((x + 0.055) / 1.055, 2.4); + } + + return result * (1 - _black) + _black; + } + + public double SampleInverseAt(double x) + { + if (_black != 0) throw new NotSupportedException(); + if (x >= 1) return 1; + + if (x <= 0.0031308) return 12.92 * x; + return 1.055 * Math.Pow(x, 1 / 2.4) - 0.055; + } + } +} \ No newline at end of file diff --git a/novideo_srgb/ToneCurve.cs b/novideo_srgb/ToneCurve.cs new file mode 100644 index 0000000..7cc69d0 --- /dev/null +++ b/novideo_srgb/ToneCurve.cs @@ -0,0 +1,8 @@ +namespace novideo_srgb +{ + public interface ToneCurve + { + double SampleAt(double x); + double SampleInverseAt(double x); + } +} \ No newline at end of file diff --git a/novideo_srgb/icon.ico b/novideo_srgb/icon.ico new file mode 100644 index 0000000..d4d4f7c Binary files /dev/null and b/novideo_srgb/icon.ico differ diff --git a/novideo_srgb/novideo_srgb.csproj b/novideo_srgb/novideo_srgb.csproj new file mode 100644 index 0000000..cf283ca --- /dev/null +++ b/novideo_srgb/novideo_srgb.csproj @@ -0,0 +1,49 @@ + + + + net7.0-windows10.0.20348.0 + enable + enable + Properties + true + true + true + 10.0.20348.0 + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + \ No newline at end of file