diff --git a/vibrance.GUI/Program.cs b/vibrance.GUI/Program.cs index 472db5b..3e8c743 100644 --- a/vibrance.GUI/Program.cs +++ b/vibrance.GUI/Program.cs @@ -19,6 +19,8 @@ static class Program private const string ErrorGraphicsAdapterUnknown = "Failed to determine your Graphic GraphicsAdapter type (NVIDIA/AMD). Make sure you have installed a proper GPU driver. Intel laptops are not supported as stated on the website. When installing your GPU driver did not work, please contact @juvlarN at twitter. Press Yes to open twitter in your browser now. Error: "; private const string ErrorGraphicsAdapterAmbiguous = "Both NVIDIA and AMD graphic drivers have been found on your system. This can happen when you recently switched your graphic card and did not uninstall the old drivers. Make sure to uninstall unused graphic drivers to keep your system safe and stable. Use the program \"Display Driver Uninstaller\" to uninstall your old drivers!\n\nPress Yes to open \"Display Driver Uninstaller\" download website now.\nPress No to quit vibranceGUI."; private const string MessageBoxCaption = "vibranceGUI Error"; + private const string SelfTestMessageBoxCaption = "vibranceGUI graphics adapter self test"; + private const string DisplayDriverUninstallerUrl = "http://www.guru3d.com/files-details/display-driver-uninstaller-download.html"; [STAThread] static void Main(string[] args) @@ -33,11 +35,44 @@ static void Main(string[] args) Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); + + // Runs before the adapter detection below on purpose: the vendor matching it covers is + // pure, so the self test stays runnable on a build agent or a reviewer's machine that + // has neither driver installed, where GetAdapter() shows an error and exits. + if (args.Contains("--selftest-gpu")) + { + MessageBox.Show(string.Join(Environment.NewLine, GraphicsAdapterFixture.Run().ToArray()), + SelfTestMessageBoxCaption, MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + NativeMethods.SetDllDirectory(CommonUtils.GetVibrance_GUI_AppDataPath()); GraphicsAdapter adapter = GraphicsAdapterHelper.GetAdapter(); + + // Captured here, while it still means something. The File.Exists calls below overwrite + // the thread's last error, degrading 126 "the specified module could not be found", + // which tells a user their driver DLL is missing, into 2 "the system cannot find the + // file specified". That message is what users are asked to paste into a bug report. + int adapterDetectionWin32Error = Marshal.GetLastWin32Error(); + Form vibranceGui = null; + // Both drivers installed is the one case the driver files cannot settle, and the case + // vibranceGUI used to refuse to start on. A stored choice wins first, then whichever + // adapter actually drives an attached display, and only then is the user asked. A + // machine that resolves to a single vendor never reaches any of this and keeps + // resolving exactly as it did. + if (GraphicsAdapterHelper.AreBothVendorDriversInstalled()) + { + adapter = ResolveInstalledDriverConflict(adapter); + if (adapter != GraphicsAdapter.Amd && adapter != GraphicsAdapter.Nvidia) + { + // Cancelled, or the fallback dialog has already had its say. + return; + } + } + if (adapter == GraphicsAdapter.Amd) { Func, Dictionary>>, IVibranceProxy> getProxy = (x, y) => new AmdDynamicVibranceProxy(Environment.Is64BitOperatingSystem @@ -70,7 +105,7 @@ static void Main(string[] args) } else if (adapter == GraphicsAdapter.Unknown) { - string errorMessage = new Win32Exception(Marshal.GetLastWin32Error()).Message; + string errorMessage = new Win32Exception(adapterDetectionWin32Error).Message; if (MessageBox.Show(ErrorGraphicsAdapterUnknown + errorMessage, MessageBoxCaption, MessageBoxButtons.YesNo, MessageBoxIcon.Error) == DialogResult.Yes) { @@ -80,11 +115,9 @@ static void Main(string[] args) } else if(adapter == GraphicsAdapter.Ambiguous) { - if(MessageBox.Show(ErrorGraphicsAdapterAmbiguous, MessageBoxCaption, MessageBoxButtons.YesNo, - MessageBoxIcon.Error) == DialogResult.Yes) - { - System.Diagnostics.Process.Start("http://www.guru3d.com/files-details/display-driver-uninstaller-download.html"); - } + // Not reachable through ResolveInstalledDriverConflict, which never lets an + // unresolved Ambiguous past it. Kept so the vendor cannot silently go unhandled. + ShowLegacyAmbiguousDriverDialog(); return; } if (args.Contains("-minimized")) @@ -97,5 +130,113 @@ static void Main(string[] args) GC.KeepAlive(mutex); } + + /// + /// Settles the case the driver files alone cannot: both vendors' DLLs are installed, so + /// the old code gave up here and told the user to uninstall a driver. A stored choice wins + /// first, then whatever adapter actually drives an attached display, and only when both of + /// those come up empty is the user asked. + /// Returns Ambiguous when the user declined to choose, which means "quit". + /// + static GraphicsAdapter ResolveInstalledDriverConflict(GraphicsAdapter detectedAdapter) + { + GraphicsAdapter storedAdapter = GraphicsAdapter.Unknown; + try + { + storedAdapter = new SettingsController().ReadGraphicsAdapterPreference(); + } + catch (Exception ex) + { + LogSafely(ex.ToString()); + } + + // A stored choice is honoured only while the hardware it names is still around, + // otherwise a user who swapped cards would be stuck on last year's answer. + if (storedAdapter != GraphicsAdapter.Unknown && GraphicsAdapterHelper.IsVendorDriverInstalled(storedAdapter)) + { + LogAdapterResolution("the stored preference", storedAdapter); + return storedAdapter; + } + + if (detectedAdapter == GraphicsAdapter.Amd || detectedAdapter == GraphicsAdapter.Nvidia) + { + LogAdapterResolution("the attached display devices", detectedAdapter); + return detectedAdapter; + } + + return AskUserForGraphicsAdapter(); + } + + static GraphicsAdapter AskUserForGraphicsAdapter() + { + try + { + using (GraphicsAdapterChooser chooser = new GraphicsAdapterChooser(GraphicsAdapterHelper.GetDisplayAdapters())) + { + if (chooser.ShowDialog() != DialogResult.OK || + (chooser.SelectedAdapter != GraphicsAdapter.Amd && chooser.SelectedAdapter != GraphicsAdapter.Nvidia)) + { + return GraphicsAdapter.Ambiguous; + } + + if (chooser.ShouldRememberChoice) + { + try + { + new SettingsController().SetGraphicsAdapterPreference(chooser.SelectedAdapter); + } + catch (Exception ex) + { + // Not being able to remember the answer is no reason not to act on it. + LogSafely(ex.ToString()); + } + } + + LogAdapterResolution("the chooser dialog", chooser.SelectedAdapter); + return chooser.SelectedAdapter; + } + } + catch (Exception ex) + { + LogSafely(ex.ToString()); + return ShowLegacyAmbiguousDriverDialog(); + } + } + + /// + /// The pre-existing dialog, now only the fallback for when the chooser itself cannot be + /// shown. Its DDU advice is right for a leftover driver and wrong for hybrid hardware, + /// which is why it is no longer the first thing a user meets. + /// + static GraphicsAdapter ShowLegacyAmbiguousDriverDialog() + { + if (MessageBox.Show(ErrorGraphicsAdapterAmbiguous, MessageBoxCaption, MessageBoxButtons.YesNo, + MessageBoxIcon.Error) == DialogResult.Yes) + { + System.Diagnostics.Process.Start(DisplayDriverUninstallerUrl); + } + return GraphicsAdapter.Ambiguous; + } + + /// + /// The line to ask a user for when they report that vibranceGUI picked the wrong GPU. + /// + static void LogAdapterResolution(string source, GraphicsAdapter adapter) + { + LogSafely(String.Format("Both GPU drivers are installed. Resolved to {0} from {1}.{2}{3}", + adapter, source, Environment.NewLine, GraphicsAdapterHelper.DescribeDisplayAdapters())); + } + + static void LogSafely(string message) + { + try + { + VibranceGUI.Log(message); + } + catch (Exception) + { + // Logging must never be the reason startup fails. + } + } } } diff --git a/vibrance.GUI/common/GraphicsAdapter.cs b/vibrance.GUI/common/GraphicsAdapter.cs index bcd684c..acd34d0 100644 --- a/vibrance.GUI/common/GraphicsAdapter.cs +++ b/vibrance.GUI/common/GraphicsAdapter.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; +using System.Text; using vibrance.GUI.AMD.vendor; using vibrance.GUI.AMD.vendor.adl32; using vibrance.GUI.NVIDIA; @@ -15,12 +17,64 @@ public enum GraphicsAdapter Ambiguous = 3 } + /// + /// One graphics adapter, as Windows reports it through EnumDisplayDevices. Windows emits an + /// adapter entry per display head, so several entries share a Name - they are folded into one + /// instance here and DisplayNames lists every head the adapter owns. + /// Only ever produced by GraphicsAdapterHelper.GetDisplayAdapters(). + /// + public class DisplayAdapterInfo + { + public string Name { get; set; } // DeviceString, e.g. "NVIDIA GeForce RTX 5070 Ti" + public GraphicsAdapter Vendor { get; set; } // Nvidia, Amd, or Unknown for anything else + public bool IsAttachedToDesktop { get; set; } // drives at least one display of the desktop + public bool IsPrimary { get; set; } // owns the primary display + public List DisplayNames { get; set; } // "\\.\DISPLAY1", ...; diagnostics only + + public DisplayAdapterInfo() + { + DisplayNames = new List(); + } + } + public class GraphicsAdapterHelper { [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LoadLibrary(string dllToLoad); + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern bool EnumDisplayDevices(string lpDevice, uint iDevNum, ref DisplayDevice lpDisplayDevice, uint dwFlags); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct DisplayDevice + { + public int cb; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] + public string DeviceName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string DeviceString; + public uint StateFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string DeviceID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string DeviceKey; + } + + private const uint DisplayDeviceAttachedToDesktop = 0x00000001; + private const uint DisplayDevicePrimaryDevice = 0x00000004; + + // Nothing real comes close to this. The bound only exists so that a display driver which + // never fails the enumeration cannot spin the loop forever before the window opens. + private const uint MaxEnumeratedDisplayDevices = 64; + + // Matched case-insensitively as whole words in the adapter's DeviceString - see + // ContainsAnyToken for why the word boundary is not optional. Anything that matches + // neither list - Intel above all, and every virtual display driver - is Unknown and must + // never be reported as one of the two supported vendors. + private static readonly string[] NvidiaAdapterNameTokens = { "NVIDIA" }; + private static readonly string[] AmdAdapterNameTokens = { "AMD", "Radeon", "ATI" }; + private const string _nvidiaDllName = "nvapi.dll"; private static readonly string _amdDllName = Environment.Is64BitOperatingSystem ? AMD.vendor.adl64.AdlImport.AtiadlFileName @@ -29,11 +83,15 @@ public class GraphicsAdapterHelper public static GraphicsAdapter GetAdapter() { - string windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); - if (File.Exists(Path.Combine(windowsFolder, _amdDllName)) && - File.Exists(Path.Combine(windowsFolder, _nvidiaDllName))) + if (AreBothVendorDriversInstalled()) { - return GraphicsAdapter.Ambiguous; + // A driver DLL sitting in the system folder says nothing about whether that GPU is + // in use, so the file system alone cannot settle this. Ask Windows which adapter + // actually drives a display first: on an AMD CPU with integrated graphics plus a + // discrete NVIDIA card - both DLLs present, one GPU driving the monitors - there + // is an unambiguous answer, and it is the difference between the application + // starting and refusing to start at all. + return GetAdapterFromAttachedDisplays(); } if (IsAdapterAvailable(_amdDllName)) { @@ -50,6 +108,255 @@ public static GraphicsAdapter GetAdapter() return GraphicsAdapter.Unknown; } + /// + /// True when both vendors' driver DLLs are installed. That is the only case GetAdapter() + /// cannot decide from the file system, and therefore the only case in which the display + /// device detection, a stored preference or the chooser are allowed to have a say - a + /// machine that resolves to a single vendor today keeps resolving exactly as it did. + /// + public static bool AreBothVendorDriversInstalled() + { + return IsVendorDriverInstalled(GraphicsAdapter.Amd) && IsVendorDriverInstalled(GraphicsAdapter.Nvidia); + } + + /// + /// True when the driver DLL of the given vendor is installed. Used to discard a stored + /// preference that names hardware the user no longer has. + /// + public static bool IsVendorDriverInstalled(GraphicsAdapter graphicsAdapter) + { + string dllName; + if (graphicsAdapter == GraphicsAdapter.Nvidia) + { + dllName = _nvidiaDllName; + } + else if (graphicsAdapter == GraphicsAdapter.Amd) + { + dllName = _amdDllName; + } + else + { + return false; + } + + try + { + string windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.SystemX86); + return File.Exists(Path.Combine(windowsFolder, dllName)); + } + catch (Exception) + { + return false; + } + } + + /// + /// The vendor of the adapter that actually drives a display attached to the desktop. This + /// is the signal that discriminates a hybrid machine: Win32_VideoController reports an AMD + /// iGPU and a discrete NVIDIA card as equally OK, but only one of them has a monitor on + /// it. Returns Ambiguous when both vendors drive a display, and also when neither does - + /// an Intel-only desktop, an RDP session, or an enumeration that told us nothing. In those + /// cases asking is better than guessing. + /// + public static GraphicsAdapter GetAdapterFromAttachedDisplays() + { + bool isNvidiaAttached = false; + bool isAmdAttached = false; + foreach (DisplayAdapterInfo adapter in GetAttachedDisplayAdapters()) + { + if (adapter.Vendor == GraphicsAdapter.Nvidia) + { + isNvidiaAttached = true; + } + else if (adapter.Vendor == GraphicsAdapter.Amd) + { + isAmdAttached = true; + } + } + + if (isNvidiaAttached && !isAmdAttached) + { + return GraphicsAdapter.Nvidia; + } + if (isAmdAttached && !isNvidiaAttached) + { + return GraphicsAdapter.Amd; + } + return GraphicsAdapter.Ambiguous; + } + + /// + /// The adapters that drive at least one display attached to the desktop. + /// + public static List GetAttachedDisplayAdapters() + { + List attachedAdapters = new List(); + foreach (DisplayAdapterInfo adapter in GetDisplayAdapters()) + { + if (adapter.IsAttachedToDesktop) + { + attachedAdapters.Add(adapter); + } + } + return attachedAdapters; + } + + /// + /// Every graphics adapter Windows knows about, folded to one entry per adapter name. + /// Never throws and never returns null: this runs before the main window exists, and a + /// virtual display driver, an RDP session or a headless machine can make EnumDisplayDevices + /// behave in ways no caller should have to anticipate. Every caller treats an empty list + /// as "could not tell", which falls back to the behaviour that was there before. + /// + public static List GetDisplayAdapters() + { + List adapters = new List(); + bool isEnumerationComplete = false; + try + { + for (uint deviceIndex = 0; deviceIndex < MaxEnumeratedDisplayDevices; deviceIndex++) + { + DisplayDevice device = new DisplayDevice(); + device.cb = Marshal.SizeOf(typeof(DisplayDevice)); + if (!EnumDisplayDevices(null, deviceIndex, ref device, 0)) + { + isEnumerationComplete = true; + break; + } + + string adapterName = device.DeviceString == null ? string.Empty : device.DeviceString.Trim(); + if (adapterName.Length == 0) + { + continue; + } + + DisplayAdapterInfo adapter = FindAdapterByName(adapters, adapterName); + if (adapter == null) + { + adapter = new DisplayAdapterInfo(); + adapter.Name = adapterName; + adapter.Vendor = GetVendorFromAdapterName(adapterName); + adapters.Add(adapter); + } + + adapter.IsAttachedToDesktop |= (device.StateFlags & DisplayDeviceAttachedToDesktop) != 0; + adapter.IsPrimary |= (device.StateFlags & DisplayDevicePrimaryDevice) != 0; + if (device.DeviceName != null && device.DeviceName.Trim().Length > 0) + { + adapter.DisplayNames.Add(device.DeviceName.Trim()); + } + } + } + catch (Exception) + { + // A half-read enumeration is worse than none: it could show one vendor and hide + // the other. Report "could not tell" instead. + return new List(); + } + + if (!isEnumerationComplete) + { + // Ran out at the bound rather than at the end of the list. Same half-read hazard + // as an exception, so it gets the same answer. + return new List(); + } + return adapters; + } + + /// + /// The vendor an adapter name belongs to, or Unknown when it is neither of the two + /// supported ones. + /// + public static GraphicsAdapter GetVendorFromAdapterName(string adapterName) + { + if (string.IsNullOrEmpty(adapterName)) + { + return GraphicsAdapter.Unknown; + } + if (ContainsAnyToken(adapterName, NvidiaAdapterNameTokens)) + { + return GraphicsAdapter.Nvidia; + } + if (ContainsAnyToken(adapterName, AmdAdapterNameTokens)) + { + return GraphicsAdapter.Amd; + } + return GraphicsAdapter.Unknown; + } + + /// + /// One line per adapter, for vibranceGUI.log. This is the first thing to ask a user for + /// when they report that the wrong GPU was picked. + /// + public static string DescribeDisplayAdapters() + { + StringBuilder description = new StringBuilder(); + List adapters = GetDisplayAdapters(); + if (adapters.Count == 0) + { + return "No display adapters could be enumerated."; + } + + foreach (DisplayAdapterInfo adapter in adapters) + { + description.AppendFormat(" {0} [vendor={1}, attached={2}, primary={3}, displays={4}]", + adapter.Name, + adapter.Vendor, + adapter.IsAttachedToDesktop, + adapter.IsPrimary, + string.Join(", ", adapter.DisplayNames.ToArray())); + description.AppendLine(); + } + return description.ToString().TrimEnd(); + } + + private static DisplayAdapterInfo FindAdapterByName(List adapters, string adapterName) + { + foreach (DisplayAdapterInfo adapter in adapters) + { + if (string.Equals(adapter.Name, adapterName, StringComparison.OrdinalIgnoreCase)) + { + return adapter; + } + } + return null; + } + + /// + /// True when one of the tokens appears in the adapter name as a whole word. + /// The boundary check is not cosmetic. "ATI" occurs inside ordinary English words - + /// workstation, application, cinematic, innovation - and a bare substring match turns + /// "Workstation Virtual Display" into an AMD adapter. That would make the one branch which + /// is supposed to refuse to guess guess confidently and wrongly, building an AMD proxy on + /// a machine with no usable AMD GPU instead of showing the chooser. + /// Only letters break a match, never digits: "ATI2VGA" and "AMD780G Integrated Graphics" + /// are real adapter names and have to keep matching. + /// + private static bool ContainsAnyToken(string adapterName, string[] tokens) + { + foreach (string token in tokens) + { + int index = adapterName.IndexOf(token, StringComparison.OrdinalIgnoreCase); + while (index >= 0) + { + if (IsWordBoundary(adapterName, index - 1) && + IsWordBoundary(adapterName, index + token.Length)) + { + return true; + } + // Keep looking: an earlier glued occurrence must not hide a later real one, + // as in "Innovation Radeon Display". + index = adapterName.IndexOf(token, index + 1, StringComparison.OrdinalIgnoreCase); + } + } + return false; + } + + private static bool IsWordBoundary(string adapterName, int index) + { + return index < 0 || index >= adapterName.Length || !char.IsLetter(adapterName[index]); + } + private static bool IsAdapterAvailable(string dllName) { try diff --git a/vibrance.GUI/common/GraphicsAdapterChooser.Designer.cs b/vibrance.GUI/common/GraphicsAdapterChooser.Designer.cs new file mode 100644 index 0000000..8598037 --- /dev/null +++ b/vibrance.GUI/common/GraphicsAdapterChooser.Designer.cs @@ -0,0 +1,167 @@ +namespace vibrance.GUI.common +{ + partial class GraphicsAdapterChooser + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.labelHeadline = new System.Windows.Forms.Label(); + this.labelExplanation = new System.Windows.Forms.Label(); + this.listViewAdapters = new System.Windows.Forms.ListView(); + this.checkBoxRemember = new System.Windows.Forms.CheckBox(); + this.labelDdu = new System.Windows.Forms.Label(); + this.linkLabelDdu = new System.Windows.Forms.LinkLabel(); + this.buttonUse = new System.Windows.Forms.Button(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // labelHeadline + // + this.labelHeadline.AutoSize = true; + this.labelHeadline.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.labelHeadline.Location = new System.Drawing.Point(12, 12); + this.labelHeadline.Name = "labelHeadline"; + this.labelHeadline.Size = new System.Drawing.Size(297, 16); + this.labelHeadline.TabIndex = 0; + this.labelHeadline.Text = "Which graphics card should vibranceGUI control?"; + // + // labelExplanation + // + this.labelExplanation.Location = new System.Drawing.Point(12, 38); + this.labelExplanation.Name = "labelExplanation"; + this.labelExplanation.Size = new System.Drawing.Size(580, 45); + this.labelExplanation.TabIndex = 1; + this.labelExplanation.Text = "An NVIDIA and an AMD driver are both installed here, and vibranceGUI could not wor" + + "k out on its own which card is driving your screen. Pick the one whose color sett" + + "ings it should change."; + // + // listViewAdapters + // + this.listViewAdapters.FullRowSelect = true; + this.listViewAdapters.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.Nonclickable; + this.listViewAdapters.HideSelection = false; + this.listViewAdapters.Location = new System.Drawing.Point(12, 90); + this.listViewAdapters.MultiSelect = false; + this.listViewAdapters.Name = "listViewAdapters"; + this.listViewAdapters.Size = new System.Drawing.Size(580, 160); + this.listViewAdapters.TabIndex = 2; + this.listViewAdapters.UseCompatibleStateImageBehavior = false; + this.listViewAdapters.View = System.Windows.Forms.View.Details; + this.listViewAdapters.SelectedIndexChanged += new System.EventHandler(this.listViewAdapters_SelectedIndexChanged); + this.listViewAdapters.DoubleClick += new System.EventHandler(this.listViewAdapters_DoubleClick); + // + // checkBoxRemember + // + this.checkBoxRemember.AutoSize = true; + this.checkBoxRemember.Checked = true; + this.checkBoxRemember.CheckState = System.Windows.Forms.CheckState.Checked; + this.checkBoxRemember.Location = new System.Drawing.Point(12, 258); + this.checkBoxRemember.Name = "checkBoxRemember"; + this.checkBoxRemember.Size = new System.Drawing.Size(203, 17); + this.checkBoxRemember.TabIndex = 3; + this.checkBoxRemember.Text = "Remember my choice and stop asking"; + this.checkBoxRemember.UseVisualStyleBackColor = true; + // + // labelDdu + // + this.labelDdu.AutoSize = true; + this.labelDdu.ForeColor = System.Drawing.SystemColors.GrayText; + this.labelDdu.Location = new System.Drawing.Point(12, 288); + this.labelDdu.Name = "labelDdu"; + this.labelDdu.Size = new System.Drawing.Size(496, 13); + this.labelDdu.TabIndex = 4; + this.labelDdu.Text = "Swapped graphics cards and never removed the old driver? Only then is it worth cle" + + "aning up with"; + // + // linkLabelDdu + // + this.linkLabelDdu.AutoSize = true; + this.linkLabelDdu.Location = new System.Drawing.Point(12, 308); + this.linkLabelDdu.Name = "linkLabelDdu"; + this.linkLabelDdu.Size = new System.Drawing.Size(196, 13); + this.linkLabelDdu.TabIndex = 5; + this.linkLabelDdu.TabStop = true; + this.linkLabelDdu.Text = "Display Driver Uninstaller (guru3d.com)"; + this.linkLabelDdu.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabelDdu_LinkClicked); + // + // buttonUse + // + this.buttonUse.Location = new System.Drawing.Point(354, 340); + this.buttonUse.Name = "buttonUse"; + this.buttonUse.Size = new System.Drawing.Size(150, 26); + this.buttonUse.TabIndex = 6; + this.buttonUse.Text = "Use this graphics card"; + this.buttonUse.UseVisualStyleBackColor = true; + this.buttonUse.Click += new System.EventHandler(this.buttonUse_Click); + // + // buttonCancel + // + this.buttonCancel.Location = new System.Drawing.Point(512, 340); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Size = new System.Drawing.Size(80, 26); + this.buttonCancel.TabIndex = 7; + this.buttonCancel.Text = "Cancel"; + this.buttonCancel.UseVisualStyleBackColor = true; + this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); + // + // GraphicsAdapterChooser + // + this.AcceptButton = this.buttonUse; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.buttonCancel; + this.ClientSize = new System.Drawing.Size(604, 380); + this.Controls.Add(this.buttonCancel); + this.Controls.Add(this.buttonUse); + this.Controls.Add(this.linkLabelDdu); + this.Controls.Add(this.labelDdu); + this.Controls.Add(this.checkBoxRemember); + this.Controls.Add(this.listViewAdapters); + this.Controls.Add(this.labelExplanation); + this.Controls.Add(this.labelHeadline); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "GraphicsAdapterChooser"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "vibranceGUI - select your graphics card"; + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label labelHeadline; + private System.Windows.Forms.Label labelExplanation; + private System.Windows.Forms.ListView listViewAdapters; + private System.Windows.Forms.CheckBox checkBoxRemember; + private System.Windows.Forms.Label labelDdu; + private System.Windows.Forms.LinkLabel linkLabelDdu; + private System.Windows.Forms.Button buttonUse; + private System.Windows.Forms.Button buttonCancel; + } +} diff --git a/vibrance.GUI/common/GraphicsAdapterChooser.cs b/vibrance.GUI/common/GraphicsAdapterChooser.cs new file mode 100644 index 0000000..9652197 --- /dev/null +++ b/vibrance.GUI/common/GraphicsAdapterChooser.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace vibrance.GUI.common +{ + /// + /// Asked once, and only when it is genuinely unclear which GPU vibranceGUI should drive: both + /// vendors' drivers are installed AND the attached display devices did not settle it, either + /// because both vendors drive a display or because neither of them does. + /// It lists the adapter names Windows reports rather than bare vendor names, because a user + /// recognises "NVIDIA GeForce RTX 5070 Ti" and does not necessarily know which chip is in + /// their laptop. This runs before the main form exists, so it has no proxy to ask anything of. + /// + public partial class GraphicsAdapterChooser : Form + { + private const string DisplayDriverUninstallerUrl = "http://www.guru3d.com/files-details/display-driver-uninstaller-download.html"; + + private const string StatusPrimaryDisplay = "Drives your main display"; + private const string StatusAttached = "Drives a display"; + private const string StatusNotAttached = "No display attached"; + + // Shown only when Windows lists no display device at all for a vendor whose driver is + // installed. Naming the vendor is the best that can be done in that case, and it is still + // better than an empty list, which would be the dead end this dialog exists to remove. + private const string FallbackNvidiaAdapterName = "NVIDIA graphics card"; + private const string FallbackAmdAdapterName = "AMD graphics card"; + + private readonly List _adapters; + + // The pick is tracked here rather than read back from listViewAdapters.SelectedItems, + // which stays empty until the list has a window handle. Reading it straight would leave + // the accept button disabled while a row was visibly highlighted. + private ListViewItem _selectedItem; + + public GraphicsAdapterChooser(List displayAdapters) + { + InitializeComponent(); + + listViewAdapters.Columns.Add("Graphics adapter", 360, HorizontalAlignment.Left); + listViewAdapters.Columns.Add("Status", 190, HorizontalAlignment.Left); + + SelectedAdapter = GraphicsAdapter.Unknown; + ShouldRememberChoice = checkBoxRemember.Checked; + + _adapters = BuildCandidates(displayAdapters); + FillList(); + + try + { + this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(Application.ExecutablePath); + } + catch (Exception) + { + // The window icon is not worth failing a startup dialog over. + } + } + + /// + /// The vendor the user picked, or Unknown when the dialog was cancelled or closed. + /// + public GraphicsAdapter SelectedAdapter { get; private set; } + + /// + /// Whether the choice should be written to the INI so the question is asked only once. + /// + public bool ShouldRememberChoice { get; private set; } + + /// + /// The adapters worth offering: the supported ones that drive a display, or - when none of + /// them do - every supported one Windows knows about. Both vendors always end up pickable, + /// because this dialog only opens when both vendors' drivers are installed. + /// + private static List BuildCandidates(List displayAdapters) + { + List supportedAdapters = new List(); + List attachedAdapters = new List(); + if (displayAdapters != null) + { + foreach (DisplayAdapterInfo adapter in displayAdapters) + { + if (adapter == null || + (adapter.Vendor != GraphicsAdapter.Nvidia && adapter.Vendor != GraphicsAdapter.Amd)) + { + continue; + } + + supportedAdapters.Add(adapter); + if (adapter.IsAttachedToDesktop) + { + attachedAdapters.Add(adapter); + } + } + } + + List candidates = attachedAdapters.Count > 0 ? attachedAdapters : supportedAdapters; + AddFallbackAdapter(candidates, GraphicsAdapter.Nvidia, FallbackNvidiaAdapterName); + AddFallbackAdapter(candidates, GraphicsAdapter.Amd, FallbackAmdAdapterName); + return candidates; + } + + private static void AddFallbackAdapter(List candidates, GraphicsAdapter vendor, string adapterName) + { + foreach (DisplayAdapterInfo candidate in candidates) + { + if (candidate.Vendor == vendor) + { + return; + } + } + + DisplayAdapterInfo fallbackAdapter = new DisplayAdapterInfo(); + fallbackAdapter.Name = adapterName; + fallbackAdapter.Vendor = vendor; + candidates.Add(fallbackAdapter); + } + + private void FillList() + { + ListViewItem defaultItem = null; + foreach (DisplayAdapterInfo adapter in _adapters) + { + ListViewItem listItem = new ListViewItem(adapter.Name); + listItem.Tag = adapter; + listItem.SubItems.Add(DescribeStatus(adapter)); + listViewAdapters.Items.Add(listItem); + + // The adapter that owns the primary display is the one the user is looking at, so + // it is the safe default. + if (adapter.IsPrimary) + { + defaultItem = listItem; + } + } + + // Otherwise the first entry, so the dialog never opens with nothing picked. + if (defaultItem == null && listViewAdapters.Items.Count > 0) + { + defaultItem = listViewAdapters.Items[0]; + } + if (defaultItem != null) + { + _selectedItem = defaultItem; + defaultItem.Selected = true; + } + + listViewAdapters.Select(); + UpdateAcceptButton(); + } + + private static string DescribeStatus(DisplayAdapterInfo adapter) + { + if (adapter.IsPrimary) + { + return StatusPrimaryDisplay; + } + if (adapter.IsAttachedToDesktop) + { + return StatusAttached; + } + return StatusNotAttached; + } + + private DisplayAdapterInfo GetSelectedAdapter() + { + if (_selectedItem == null) + { + return null; + } + return _selectedItem.Tag as DisplayAdapterInfo; + } + + private void UpdateAcceptButton() + { + buttonUse.Enabled = GetSelectedAdapter() != null; + } + + private void listViewAdapters_SelectedIndexChanged(object sender, EventArgs e) + { + if (listViewAdapters.SelectedItems.Count == 1) + { + _selectedItem = listViewAdapters.SelectedItems[0]; + } + else if (_selectedItem != null && listViewAdapters.IsHandleCreated) + { + // Clicking past the last row clears the highlight. Put it back rather than leave + // an enabled accept button pointing at a row the user can no longer see. + _selectedItem.Selected = true; + } + UpdateAcceptButton(); + } + + private void listViewAdapters_DoubleClick(object sender, EventArgs e) + { + Accept(); + } + + private void buttonUse_Click(object sender, EventArgs e) + { + Accept(); + } + + private void buttonCancel_Click(object sender, EventArgs e) + { + this.DialogResult = DialogResult.Cancel; + this.Close(); + } + + private void Accept() + { + DisplayAdapterInfo adapter = GetSelectedAdapter(); + if (adapter == null) + { + return; + } + + SelectedAdapter = adapter.Vendor; + ShouldRememberChoice = checkBoxRemember.Checked; + this.DialogResult = DialogResult.OK; + this.Close(); + } + + private void linkLabelDdu_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + try + { + System.Diagnostics.Process.Start(DisplayDriverUninstallerUrl); + } + catch (Exception ex) + { + try + { + VibranceGUI.Log(ex); + } + catch (Exception) + { + // No browser and no log file is still no reason to take the dialog down. + } + } + } + } +} diff --git a/vibrance.GUI/common/GraphicsAdapterFixture.cs b/vibrance.GUI/common/GraphicsAdapterFixture.cs new file mode 100644 index 0000000..394f205 --- /dev/null +++ b/vibrance.GUI/common/GraphicsAdapterFixture.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; + +namespace vibrance.GUI.common +{ + /// + /// The reference expectations for GetVendorFromAdapterName, as literal data. No GUI, no + /// display devices, no driver files. Run by vibrance.GUI.exe --selftest-gpu. + /// + /// That function decides which GPU vibranceGUI drives on a machine with both drivers + /// installed, and it is cheap to get subtly wrong. The word-boundary cases below are here + /// because a bare substring match on "ATI" classified "Workstation Virtual Display" as an AMD + /// adapter: a virtual display driver would have turned an honest Ambiguous into a confident + /// wrong answer. + /// + public static class GraphicsAdapterFixture + { + public static List Run() + { + List lines = new List(); + lines.Add("vibranceGUI graphics adapter self test"); + lines.Add(string.Empty); + + int passed = 0; + int total = 0; + + lines.Add("Vendor of the adapter name Windows reports:"); + foreach (VendorCase vendorCase in BuildVendorCases()) + { + GraphicsAdapter actual = GraphicsAdapterHelper.GetVendorFromAdapterName(vendorCase.AdapterName); + bool isPass = actual == vendorCase.ExpectedVendor; + total++; + if (isPass) + passed++; + + lines.Add(string.Format("[{0}] {1} got={2} expected={3}", + isPass ? "PASS" : "FAIL", + Quote(vendorCase.AdapterName).PadRight(36), + actual.ToString().PadRight(10), + vendorCase.ExpectedVendor)); + } + + lines.Add(string.Empty); + lines.Add(string.Format("PASSED {0}/{1}", passed, total)); + lines.Add(string.Empty); + lines.Add("This reads neither the display devices nor the driver files, so it gives the same"); + lines.Add("answer on a build agent as on the machine that reported the bug."); + return lines; + } + + private static List BuildVendorCases() + { + List cases = new List(); + + cases.Add(new VendorCase("NVIDIA GeForce RTX 5070 Ti", GraphicsAdapter.Nvidia)); + cases.Add(new VendorCase("NVIDIA GeForce GTX 1080 Ti", GraphicsAdapter.Nvidia)); + cases.Add(new VendorCase("NVIDIA Quadro P2000", GraphicsAdapter.Nvidia)); + + cases.Add(new VendorCase("AMD Radeon(TM) Graphics", GraphicsAdapter.Amd)); + cases.Add(new VendorCase("AMD Radeon RX 7900 XTX", GraphicsAdapter.Amd)); + cases.Add(new VendorCase("Radeon RX 580 Series", GraphicsAdapter.Amd)); + cases.Add(new VendorCase("ATI Technologies Inc.", GraphicsAdapter.Amd)); + + // Digits must stay valid word boundaries, or these two real adapter names stop + // matching the moment the boundary check is added. + cases.Add(new VendorCase("ATI2VGA", GraphicsAdapter.Amd)); + cases.Add(new VendorCase("AMD780G Integrated Graphics", GraphicsAdapter.Amd)); + + // The regression the boundary check exists for: "ATI" inside an ordinary English word. + // Every one of these was classified as Amd by a bare substring match. + cases.Add(new VendorCase("Workstation Virtual Display", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("Application Virtual Display", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("Cinematic Display Driver", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("Innovation Display Adapter", GraphicsAdapter.Unknown)); + + // A glued occurrence must not hide a real one later in the same name. + cases.Add(new VendorCase("Innovation Radeon Display", GraphicsAdapter.Amd)); + + // Intel is neither, and saying so is the whole point of the Unknown case. + cases.Add(new VendorCase("Intel(R) UHD Graphics 770", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("Intel(R) Iris(R) Xe Graphics", GraphicsAdapter.Unknown)); + + cases.Add(new VendorCase("Microsoft Basic Display Adapter", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("Parsec Virtual Display Adapter", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("Citrix Indirect Display Adapter", GraphicsAdapter.Unknown)); + cases.Add(new VendorCase("DisplayLink USB Device", GraphicsAdapter.Unknown)); + + cases.Add(new VendorCase(string.Empty, GraphicsAdapter.Unknown)); + cases.Add(new VendorCase(null, GraphicsAdapter.Unknown)); + + return cases; + } + + private static string Quote(string adapterName) + { + return adapterName == null ? "" : "\"" + adapterName + "\""; + } + + private class VendorCase + { + public VendorCase(string adapterName, GraphicsAdapter expectedVendor) + { + this.AdapterName = adapterName; + this.ExpectedVendor = expectedVendor; + } + + public string AdapterName { get; private set; } + + public GraphicsAdapter ExpectedVendor { get; private set; } + } + } +} diff --git a/vibrance.GUI/common/ISettingsController.cs b/vibrance.GUI/common/ISettingsController.cs index 15d38da..8d3d0b9 100644 --- a/vibrance.GUI/common/ISettingsController.cs +++ b/vibrance.GUI/common/ISettingsController.cs @@ -7,6 +7,8 @@ internal interface ISettingsController { bool SetVibranceSettings(string windowsLevel, string affectPrimaryMonitorOnly, string neverSwitchResolution, List applicationSettings); bool SetVibranceSetting(string szKeyName, string value); + GraphicsAdapter ReadGraphicsAdapterPreference(); + bool SetGraphicsAdapterPreference(GraphicsAdapter graphicsAdapter); void ReadVibranceSettings(GraphicsAdapter graphicsAdapter, out int vibranceWindowsLevel, out bool affectPrimaryMonitorOnly, out bool neverSwitchResolution, out List applicationSettings); } } \ No newline at end of file diff --git a/vibrance.GUI/common/SettingsController.cs b/vibrance.GUI/common/SettingsController.cs index f7dcfee..c5e6cfd 100644 --- a/vibrance.GUI/common/SettingsController.cs +++ b/vibrance.GUI/common/SettingsController.cs @@ -32,6 +32,7 @@ private static extern bool WritePrivateProfileString(string lpAppName, const string SzKeyNameRefreshRate = "refreshRate"; const string SzKeyNameAffectPrimaryMonitorOnly = "affectPrimaryMonitorOnly"; const string SzKeyNameNeverSwitchResolution = "neverSwitchResolution"; + const string SzKeyNameGraphicsAdapter = "graphicsAdapter"; private string _fileName = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).ToString() + "\\vibranceGUI\\vibranceGUI.ini"; private string _fileNameApplicationSettings = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData).ToString() + "\\vibranceGUI\\applicationData.xml"; @@ -78,6 +79,54 @@ public bool SetVibranceSetting(string szKeyName, string value) return (Marshal.GetLastWin32Error() == 0); } + /// + /// The GPU vendor the user picked when both drivers were installed, or Unknown when the + /// INI holds no preference - which is what every existing installation looks like, and + /// what an INI written by an older version looks like too. + /// Read on its own because it is needed before the main form and the application settings + /// XML exist, so it must not go through ReadVibranceSettings. + /// + public GraphicsAdapter ReadGraphicsAdapterPreference() + { + if (!IsFileExisting(_fileName)) + { + return GraphicsAdapter.Unknown; + } + + StringBuilder szValueGraphicsAdapter = new StringBuilder(1024); + GetPrivateProfileString(SzSectionName, + SzKeyNameGraphicsAdapter, + "", + szValueGraphicsAdapter, + Convert.ToUInt32(szValueGraphicsAdapter.Capacity), + _fileName); + + string szGraphicsAdapter = szValueGraphicsAdapter.ToString().Trim(); + if (string.Equals(szGraphicsAdapter, GraphicsAdapter.Nvidia.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return GraphicsAdapter.Nvidia; + } + if (string.Equals(szGraphicsAdapter, GraphicsAdapter.Amd.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return GraphicsAdapter.Amd; + } + return GraphicsAdapter.Unknown; + } + + /// + /// Stores the vendor the user picked. Only the two supported vendors are ever written, so + /// that the key can never be turned into a value the reader would have to guess about. + /// + public bool SetGraphicsAdapterPreference(GraphicsAdapter graphicsAdapter) + { + if (graphicsAdapter != GraphicsAdapter.Nvidia && graphicsAdapter != GraphicsAdapter.Amd) + { + return false; + } + + return SetVibranceSetting(SzKeyNameGraphicsAdapter, graphicsAdapter.ToString()); + } + private bool PrepareFile() { if (!IsFileExisting(_fileName)) diff --git a/vibrance.GUI/vibrance.GUI.csproj b/vibrance.GUI/vibrance.GUI.csproj index 71e63a8..dad56c2 100644 --- a/vibrance.GUI/vibrance.GUI.csproj +++ b/vibrance.GUI/vibrance.GUI.csproj @@ -127,6 +127,13 @@ + + Form + + + GraphicsAdapterChooser.cs + +