diff --git a/.gitignore b/.gitignore index 34dc4e8..3f74515 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ *.user *.suo +*.pfx Bin/ obj/ -packages/ -.vs/ \ No newline at end of file +packages/ +.vs/ +.omo/ +4060.ps1 \ No newline at end of file diff --git a/App/GpuAppManager.cs b/App/GpuAppManager.cs index 86c7201..14c2d78 100644 --- a/App/GpuAppManager.cs +++ b/App/GpuAppManager.cs @@ -124,6 +124,7 @@ public static int GetMemoryClockOffset() { } public static int GetGraphicsBoostClock() { + NVIDIA.Initialize(); try { PhysicalGPU[] gpus = PhysicalGPU.GetPhysicalGPUs(); @@ -143,12 +144,13 @@ public static int GetGraphicsBoostClock() { } } } catch { - } + } finally { NVIDIA.Unload(); } return 0; } public static int GetMemoryBoostClock() { + NVIDIA.Initialize(); try { PhysicalGPU[] gpus = PhysicalGPU.GetPhysicalGPUs(); @@ -168,7 +170,7 @@ public static int GetMemoryBoostClock() { } } } catch { - } + } finally { NVIDIA.Unload(); } return 0; } @@ -219,7 +221,7 @@ public static void RestartGpu() { // 2. 通过 ExecuteCommand 执行 pnputil 重启设备 string command = $"pnputil /restart-device \"{instanceId}\""; - ProcessResult result = ExecuteCommand(command); + ProcessResult result = RestartGpu(instanceId); // 可选:根据结果给出提示 if (result.ExitCode != 0) { @@ -230,6 +232,14 @@ public static void RestartGpu() { } } + public static ProcessResult RestartGpu(string instanceId) { + if (string.IsNullOrWhiteSpace(instanceId)) + return new ProcessResult { ExitCode = -1, Error = Strings.DeviceNotFound }; + string safeId = instanceId.Replace(((char)34).ToString(), ""); + string command = string.Format("pnputil /restart-device {0}{1}{0}", (char)34, safeId); + return ExecuteCommand(command); + } + /// /// 获取所有显卡名称列表(跳过 Microsoft 基本显示适配器) /// @@ -313,14 +323,7 @@ public static string GetGpuModelFromNvidiaSmi() { /// 是否存在 NVIDIA 独显。 public static bool HasNvidiaGpu() { - try { - var gpus = PhysicalGPU.GetPhysicalGPUs(); - - return gpus != null && - gpus.Length > 0; - } catch { - return false; - } + return GetNvidiaGpuInfoList().Count > 0; } /// diff --git a/App/GpuSleepManager.cs b/App/GpuSleepManager.cs new file mode 100644 index 0000000..997d84d --- /dev/null +++ b/App/GpuSleepManager.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Win32; + +namespace OmenSuperHub { + public static class GpuSleepManager { + const string PreferencesPath = @"Software\Microsoft\DirectX\UserGpuPreferences"; + const string TransactionsPath = @"Software\OmenSuperHub\GpuPreferenceTransactions"; + static Guid displayClassGuid = new Guid("4d36e968-e325-11ce-bfc1-08002be10318"); + + public enum DevicePowerState { Unknown, D0, D1, D2, D3 } + public sealed class GpuDeviceInfo { + public string Name, InstanceId; + public bool IsEnabled, IsPhysical, IsNvidia, HasCurrentDisplayMode; + public DevicePowerState PowerState; + } + public sealed class GpuProcessInfo { + public int ProcessId; + public string ProcessName, ExecutablePath, UsageType, PreferenceRawValue, PreferenceDisplay; + public bool IsSystemProcess, CanConfigurePreference, PreferenceEntryExisted; + } + public sealed class PreferenceEntry { + public string ExecutablePath, ProcessName, OriginalValue, AppliedValue; + public bool OriginalEntryExisted; + } + public sealed class PreferenceTransaction { + public string Id; + public readonly List Entries = new List(); + } + public sealed class RestoreResult { + public bool Success; + public string Error; + public readonly List RestoredPaths = new List(); + public readonly List ConflictedPaths = new List(); + } + + public static List GetDisplayAdapters() { + var result = new List(); + try { + using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController")) + using (var objects = searcher.Get()) { + foreach (ManagementObject item in objects) { + string id = item["PNPDeviceID"]?.ToString() ?? ""; + string name = item["Name"]?.ToString() ?? ""; + int error = -1; + if (item["ConfigManagerErrorCode"] != null) int.TryParse(item["ConfigManagerErrorCode"].ToString(), out error); + result.Add(new GpuDeviceInfo { + Name = name, + InstanceId = id, + IsEnabled = error == 0, + IsPhysical = id.StartsWith("PCI" + (char)92, StringComparison.OrdinalIgnoreCase) && !ContainsAny(name, "Microsoft", "Remote", "Virtual"), + IsNvidia = id.IndexOf("VEN_10DE", StringComparison.OrdinalIgnoreCase) >= 0 || ContainsAny(name, "NVIDIA"), + HasCurrentDisplayMode = item["CurrentHorizontalResolution"] != null, + PowerState = GetDevicePowerState(id) + }); + } + } + } catch (Exception ex) { Logger.Error("GPU enumeration failed: " + ex.Message); } + return result; + } + + public static bool TryGetNvidiaSleepTarget(out GpuDeviceInfo target, out string reason) { + target = null; + reason = null; + List enabled = GetDisplayAdapters().Where(g => g.IsPhysical && g.IsEnabled).ToList(); + if (enabled.Count < 2) { reason = Strings.GpuSleepNeedsSecondGpu; return false; } + target = enabled.FirstOrDefault(g => g.IsNvidia); + if (target == null) { reason = Strings.GpuSleepTargetNotFound; return false; } + if (target.HasCurrentDisplayMode) { reason = Strings.GpuSleepTargetHasActiveDisplay; return false; } + string targetId = target.InstanceId; + if (!enabled.Any(g => !Same(g.InstanceId, targetId) && g.HasCurrentDisplayMode)) { + reason = Strings.GpuSleepNoActiveFallbackGpu; + return false; + } + return true; + } + + public static List GetNvidiaProcesses() { + var found = new Dictionary(); + try { + var command = GpuAppManager.ExecuteCommand("nvidia-smi"); + if (command.ExitCode == 0) { + var rx = new Regex(@"^\|\s*\d+\s+\S+\s+\S+\s+(?\d+)\s+(?[A-Z+]+)\s+(?.+?)\s+(?:N/A|\d+MiB)\s*\|\s*$", RegexOptions.IgnoreCase); + foreach (string line in command.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { + Match m = rx.Match(line); + if (m.Success && int.TryParse(m.Groups["pid"].Value, out int pid)) AddProcess(found, pid, m.Groups["name"].Value.Trim(), m.Groups["type"].Value); + } + } + } catch (Exception ex) { Logger.Error("NVIDIA process query failed: " + ex.Message); } + try { + var command = GpuAppManager.ExecuteCommand("nvidia-smi --query-compute-apps=pid,process_name --format=csv,noheader"); + if (command.ExitCode == 0) foreach (string line in command.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) { + int comma = line.IndexOf(','); + if (comma > 0 && int.TryParse(line.Substring(0, comma).Trim(), out int pid)) AddProcess(found, pid, line.Substring(comma + 1).Trim(), "C"); + } + } catch { } + foreach (GpuProcessInfo process in found.Values) PopulateProcess(process); + return found.Values.OrderBy(p => p.IsSystemProcess).ThenBy(p => p.ProcessName).ToList(); + } + + public static string ApplyPowerSavingPreferences(IEnumerable processes) { + List selected = processes.Where(p => p != null && p.CanConfigurePreference) + .GroupBy(p => p.ExecutablePath, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList(); + if (selected.Count == 0) return null; + string id = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff") + "-" + Guid.NewGuid().ToString("N").Substring(0, 8); + using (RegistryKey transaction = Registry.CurrentUser.CreateSubKey(TransactionsPath + (char)92 + id)) + using (RegistryKey preferences = Registry.CurrentUser.CreateSubKey(PreferencesPath)) { + transaction.SetValue("CreatedUtc", DateTime.UtcNow.ToString("O")); + int index = 0; + foreach (GpuProcessInfo process in selected) { + object originalObject = preferences.GetValue(process.ExecutablePath, null, RegistryValueOptions.DoNotExpandEnvironmentNames); + string original = originalObject?.ToString() ?? ""; + string applied = SetPreference(original, 1); + using (RegistryKey entry = transaction.CreateSubKey((index++).ToString())) { + entry.SetValue("Path", process.ExecutablePath); + entry.SetValue("Name", process.ProcessName ?? ""); + entry.SetValue("Existed", originalObject != null ? 1 : 0); + entry.SetValue("Original", original); + entry.SetValue("Applied", applied); + } + preferences.SetValue(process.ExecutablePath, applied, RegistryValueKind.String); + } + } + return id; + } + + public static PreferenceTransaction LoadTransaction(string id) { + if (string.IsNullOrWhiteSpace(id)) return null; + using (RegistryKey transaction = Registry.CurrentUser.OpenSubKey(TransactionsPath + (char)92 + id)) { + if (transaction == null) return null; + var result = new PreferenceTransaction { Id = id }; + foreach (string child in transaction.GetSubKeyNames().OrderBy(n => n)) using (RegistryKey entry = transaction.OpenSubKey(child)) { + result.Entries.Add(new PreferenceEntry { + ExecutablePath = entry.GetValue("Path", "").ToString(), + ProcessName = entry.GetValue("Name", "").ToString(), + OriginalEntryExisted = Convert.ToInt32(entry.GetValue("Existed", 0)) != 0, + OriginalValue = entry.GetValue("Original", "").ToString(), + AppliedValue = entry.GetValue("Applied", "").ToString() + }); + } + return result; + } + } + + public static string GetLatestTransactionId() { + using (RegistryKey root = Registry.CurrentUser.OpenSubKey(TransactionsPath)) return root?.GetSubKeyNames().OrderByDescending(n => n).FirstOrDefault(); + } + + public static RestoreResult RestorePreferences(string id, bool overwriteConflicts) { + var result = new RestoreResult(); + try { + PreferenceTransaction transaction = LoadTransaction(id); + if (transaction == null) { result.Error = Strings.GpuPreferenceBackupMissing; return result; } + using (RegistryKey preferences = Registry.CurrentUser.CreateSubKey(PreferencesPath)) { + if (!overwriteConflicts) foreach (PreferenceEntry entry in transaction.Entries) { + string current = preferences.GetValue(entry.ExecutablePath, "", RegistryValueOptions.DoNotExpandEnvironmentNames)?.ToString() ?? ""; + if (!Same(current, entry.AppliedValue)) result.ConflictedPaths.Add(entry.ExecutablePath); + } + if (result.ConflictedPaths.Count > 0) return result; + foreach (PreferenceEntry entry in transaction.Entries) { + if (entry.OriginalEntryExisted) preferences.SetValue(entry.ExecutablePath, entry.OriginalValue ?? "", RegistryValueKind.String); + else preferences.DeleteValue(entry.ExecutablePath, false); + result.RestoredPaths.Add(entry.ExecutablePath); + } + } + if (result.ConflictedPaths.Count == 0 || overwriteConflicts) { + Registry.CurrentUser.DeleteSubKeyTree(TransactionsPath + (char)92 + id, false); + result.Success = true; + } + } catch (Exception ex) { result.Error = ex.Message; Logger.Error("GPU preference restore failed: " + ex.Message); } + return result; + } + + public static void RestartApplications(IEnumerable applications, bool force) { + foreach (GpuProcessInfo app in applications.GroupBy(p => p.ExecutablePath ?? p.ProcessName, StringComparer.OrdinalIgnoreCase).Select(g => g.First())) { + try { + string processName = (app.ProcessName ?? "").Replace(".exe", "").Trim(); + if (Same(processName, "dwm")) { + Process dwm = app.ProcessId > 0 ? Process.GetProcessById(app.ProcessId) : Process.GetProcessesByName("dwm").FirstOrDefault(); + dwm?.Kill(); + continue; + } + foreach (Process process in Process.GetProcessesByName(processName)) { + bool closed = false; + try { if (process.CloseMainWindow()) closed = process.WaitForExit(3000); } catch { } + if (!closed && force) try { process.Kill(); process.WaitForExit(3000); } catch { } + } + if (Same(processName, "explorer")) Process.Start(new ProcessStartInfo(Environment.ExpandEnvironmentVariables(@"%WINDIR%\explorer.exe")) { UseShellExecute = true }); + else if (!string.IsNullOrWhiteSpace(app.ExecutablePath) && System.IO.File.Exists(app.ExecutablePath)) Process.Start(new ProcessStartInfo(app.ExecutablePath) { UseShellExecute = true, WorkingDirectory = System.IO.Path.GetDirectoryName(app.ExecutablePath) }); + } catch (Exception ex) { Logger.Error("Application restart failed for " + app.ProcessName + ": " + ex.Message); } + } + } + + public static DevicePowerState GetDevicePowerState(string instanceId) { + if (string.IsNullOrWhiteSpace(instanceId)) return DevicePowerState.Unknown; + IntPtr set = SetupDiGetClassDevs(ref displayClassGuid, null, IntPtr.Zero, 2); + if (set == new IntPtr(-1)) return DevicePowerState.Unknown; + try { + uint index = 0; + var data = new SP_DEVINFO_DATA { cbSize = (uint)Marshal.SizeOf(typeof(SP_DEVINFO_DATA)) }; + while (SetupDiEnumDeviceInfo(set, index++, ref data)) { + var id = new StringBuilder(512); + if (!SetupDiGetDeviceInstanceId(set, ref data, id, id.Capacity, out _) || !Same(id.ToString(), instanceId)) continue; + byte[] buffer = new byte[128]; + if (!SetupDiGetDeviceRegistryProperty(set, ref data, 0x1E, out _, buffer, (uint)buffer.Length, out uint needed) || needed < 8) return DevicePowerState.Unknown; + int state = BitConverter.ToInt32(buffer, 4); + return state >= 1 && state <= 4 ? (DevicePowerState)state : DevicePowerState.Unknown; + } + } finally { SetupDiDestroyDeviceInfoList(set); } + return DevicePowerState.Unknown; + } + + static void AddProcess(Dictionary found, int pid, string name, string usage) { + if (!found.TryGetValue(pid, out GpuProcessInfo process)) found[pid] = new GpuProcessInfo { ProcessId = pid, ProcessName = name, UsageType = usage }; + else if ((process.UsageType ?? "").IndexOf(usage, StringComparison.OrdinalIgnoreCase) < 0) process.UsageType += "+" + usage; + } + + static void PopulateProcess(GpuProcessInfo info) { + try { using (Process process = Process.GetProcessById(info.ProcessId)) { info.ProcessName = process.ProcessName + ".exe"; try { info.ExecutablePath = process.MainModule?.FileName; } catch { } } } catch { } + string name = (info.ProcessName ?? "").ToLowerInvariant(); + info.IsSystemProcess = name == "dwm.exe" || name == "explorer.exe" || name == "csrss.exe" || name == "winlogon.exe" || name == "system"; + info.CanConfigurePreference = !string.IsNullOrWhiteSpace(info.ExecutablePath) && name != "csrss.exe" && name != "winlogon.exe" && name != "system"; + using (RegistryKey key = Registry.CurrentUser.OpenSubKey(PreferencesPath)) { + object raw = key?.GetValue(info.ExecutablePath ?? "", null, RegistryValueOptions.DoNotExpandEnvironmentNames); + info.PreferenceEntryExisted = raw != null; + info.PreferenceRawValue = raw?.ToString() ?? ""; + } + Match match = Regex.Match(info.PreferenceRawValue, @"(?:^|;)\s*GpuPreference=(\d+)", RegexOptions.IgnoreCase); + info.PreferenceDisplay = !match.Success ? Strings.GpuPreferenceSystemDefault : match.Groups[1].Value == "1" ? Strings.GpuPreferencePowerSaving : match.Groups[1].Value == "2" ? Strings.GpuPreferenceHighPerformance : Strings.GpuPreferenceSystemDefault; + } + + static string SetPreference(string raw, int preference) { + string value = Regex.Replace(raw ?? "", @"(?:^|;)\s*GpuPreference=\d+;?", ";", RegexOptions.IgnoreCase).Trim(';', ' '); + if (value.Length > 0) value += ";"; + return value + "GpuPreference=" + preference + ";"; + } + static bool ContainsAny(string value, params string[] terms) { return terms.Any(t => (value ?? "").IndexOf(t, StringComparison.OrdinalIgnoreCase) >= 0); } + static bool Same(string a, string b) { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } + + [StructLayout(LayoutKind.Sequential)] struct SP_DEVINFO_DATA { public uint cbSize; public Guid ClassGuid; public uint DevInst; public IntPtr Reserved; } + [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern IntPtr SetupDiGetClassDevs(ref Guid guid, string enumerator, IntPtr parent, uint flags); + [DllImport("setupapi.dll", SetLastError = true)] static extern bool SetupDiEnumDeviceInfo(IntPtr set, uint index, ref SP_DEVINFO_DATA data); + [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern bool SetupDiGetDeviceInstanceId(IntPtr set, ref SP_DEVINFO_DATA data, StringBuilder id, int size, out int required); + [DllImport("setupapi.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern bool SetupDiGetDeviceRegistryProperty(IntPtr set, ref SP_DEVINFO_DATA data, uint property, out uint type, byte[] buffer, uint size, out uint required); + [DllImport("setupapi.dll", SetLastError = true)] static extern bool SetupDiDestroyDeviceInfoList(IntPtr set); + } +} diff --git a/GpuSleepRepairForm.cs b/GpuSleepRepairForm.cs new file mode 100644 index 0000000..a330d95 --- /dev/null +++ b/GpuSleepRepairForm.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; + +namespace OmenSuperHub { + public sealed class GpuSleepRepairForm : Form { + readonly List processes; + readonly ListView processList; + readonly Button applyButton, restartButton, retryButton, restoreButton; + public string TransactionId { get; private set; } + + public GpuSleepRepairForm(IEnumerable detected) { + processes = detected?.ToList() ?? new List(); + Text = Strings.GpuSleepRepairTitle; + StartPosition = FormStartPosition.CenterScreen; + Size = new Size(920, 520); + MinimizeBox = false; + MaximizeBox = false; + + var description = new Label { + Dock = DockStyle.Top, + Height = 58, + Padding = new Padding(10), + Text = Strings.GpuSleepRepairDescription + }; + processList = new ListView { + Dock = DockStyle.Fill, + CheckBoxes = true, + FullRowSelect = true, + GridLines = true, + View = View.Details + }; + processList.Columns.Add(Strings.GpuSleepProcessColumn, 170); + processList.Columns.Add("PID", 70); + processList.Columns.Add(Strings.GpuSleepUsageColumn, 90); + processList.Columns.Add(Strings.GpuSleepPreferenceColumn, 130); + processList.Columns.Add(Strings.GpuSleepCustomListColumn, 90); + processList.Columns.Add(Strings.GpuSleepPathColumn, 330); + foreach (GpuSleepManager.GpuProcessInfo process in processes) { + var item = new ListViewItem(process.ProcessName ?? "?") { Tag = process, Checked = process.CanConfigurePreference && !process.IsSystemProcess }; + item.SubItems.Add(process.ProcessId.ToString()); + item.SubItems.Add(process.UsageType ?? "?"); + item.SubItems.Add(process.PreferenceDisplay ?? Strings.GpuPreferenceUnavailable); + item.SubItems.Add(process.PreferenceEntryExisted ? Strings.Yes : Strings.No); + item.SubItems.Add(process.ExecutablePath ?? Strings.GpuPreferenceUnavailable); + if (!process.CanConfigurePreference) item.ForeColor = Color.Gray; + processList.Items.Add(item); + } + + var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 52, FlowDirection = FlowDirection.RightToLeft, Padding = new Padding(6) }; + var closeButton = new Button { Text = Strings.Cancel, AutoSize = true }; + closeButton.Click += (s, e) => { DialogResult = DialogResult.Cancel; Close(); }; + retryButton = new Button { Text = Strings.GpuSleepRetry, AutoSize = true, Enabled = false }; + retryButton.Click += (s, e) => { DialogResult = DialogResult.OK; Close(); }; + restartButton = new Button { Text = Strings.GpuSleepRestartSelected, AutoSize = true, Enabled = false }; + restartButton.Click += RestartSelected; + restoreButton = new Button { Text = Strings.GpuPreferenceRestore, AutoSize = true, Enabled = false }; + restoreButton.Click += RestorePreferences; + applyButton = new Button { Text = Strings.GpuPreferenceApplyPowerSaving, AutoSize = true }; + applyButton.Click += ApplyPreferences; + buttons.Controls.Add(closeButton); + buttons.Controls.Add(retryButton); + buttons.Controls.Add(restartButton); + buttons.Controls.Add(restoreButton); + buttons.Controls.Add(applyButton); + Controls.Add(processList); + Controls.Add(description); + Controls.Add(buttons); + } + + List SelectedProcesses() { + return processList.CheckedItems.Cast() + .Select(i => i.Tag as GpuSleepManager.GpuProcessInfo) + .Where(p => p != null && p.CanConfigurePreference).ToList(); + } + + void ApplyPreferences(object sender, EventArgs e) { + List selected = SelectedProcesses(); + if (selected.Count == 0) { MessageBox.Show(this, Strings.GpuSleepSelectApplication, Strings.Hint); return; } + if (selected.Any(p => string.Equals(p.ProcessName, "dwm.exe", StringComparison.OrdinalIgnoreCase)) && + MessageBox.Show(this, Strings.GpuSleepDwmWarning, Strings.Warning, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; + try { + TransactionId = GpuSleepManager.ApplyPowerSavingPreferences(selected); + if (string.IsNullOrEmpty(TransactionId)) return; + applyButton.Enabled = false; + restartButton.Enabled = true; + restoreButton.Enabled = true; + MessageBox.Show(this, Strings.GpuPreferenceAppliedRestartRequired, Strings.Hint, MessageBoxButtons.OK, MessageBoxIcon.Information); + } catch (Exception ex) { + MessageBox.Show(this, Strings.GpuPreferenceApplyFailed(ex.Message), Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + void RestartSelected(object sender, EventArgs e) { + List selected = SelectedProcesses(); + if (MessageBox.Show(this, Strings.GpuSleepRestartWarning, Strings.Warning, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; + GpuSleepManager.RestartApplications(selected, true); + retryButton.Enabled = true; + MessageBox.Show(this, Strings.GpuSleepApplicationsRestarted, Strings.Hint, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + void RestorePreferences(object sender, EventArgs e) { + if (string.IsNullOrEmpty(TransactionId)) return; + GpuSleepManager.RestoreResult result = GpuSleepManager.RestorePreferences(TransactionId, false); + if (result.ConflictedPaths.Count > 0) { + if (MessageBox.Show(this, Strings.GpuPreferenceConflict, Strings.Warning, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; + result = GpuSleepManager.RestorePreferences(TransactionId, true); + } + if (!result.Success) { MessageBox.Show(this, result.Error ?? Strings.GpuPreferenceRestoreFailed, Strings.Error); return; } + MessageBox.Show(this, Strings.GpuPreferenceRestoredRestartRequired, Strings.Hint); + if (MessageBox.Show(this, Strings.GpuSleepRestartWarning, Strings.Warning, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) + GpuSleepManager.RestartApplications(SelectedProcesses(), true); + TransactionId = null; + restoreButton.Enabled = false; + restartButton.Enabled = false; + retryButton.Enabled = false; + applyButton.Enabled = true; + } + } +} diff --git a/OmenSuperHub.csproj b/OmenSuperHub.csproj index 64da0a4..0661fa2 100644 --- a/OmenSuperHub.csproj +++ b/OmenSuperHub.csproj @@ -39,6 +39,7 @@ false true true + true true @@ -91,9 +92,6 @@ 0FF25508E82D933D2889D9605B7F06A55C7283BB - - OmenSuperHub_TemporaryKey.pfx - false @@ -141,6 +139,9 @@ Resources\PerformanceControl.dll + + packages\System.Resources.Extensions.4.7.0\lib\netstandard2.0\System.Resources.Extensions.dll + packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll True @@ -304,6 +305,7 @@ Form + @@ -338,7 +340,6 @@ - @@ -393,6 +394,10 @@ + + + Form + @@ -414,4 +419,4 @@ - \ No newline at end of file + diff --git a/OmenSuperHub_TemporaryKey.pfx b/OmenSuperHub_TemporaryKey.pfx deleted file mode 100644 index aeb8d1c..0000000 Binary files a/OmenSuperHub_TemporaryKey.pfx and /dev/null differ diff --git a/Program.Config.cs b/Program.Config.cs index f458672..2fac626 100644 --- a/Program.Config.cs +++ b/Program.Config.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading; +using System.Windows.Forms; using Hp.Bridge.Client.SDKs.PerformanceControl.DataStructure; using HP.Omen.Core.Common.NVidiaApi; using HP.Omen.Core.Model.Device.Models; @@ -341,6 +342,10 @@ public static void CleanUpAndRemoveTasks() { static void RestoreCPUPower() { // 恢复CPU功耗设定 + if (currentPreset == FanLockPresetKey && fanLockCurrentPower >= 10 && fanLockCurrentPower <= 254) { + SetCpuPowerLimit((byte)fanLockCurrentPower); + return; + } if (cpuPower.Contains(" W")) { int value = int.Parse(cpuPower.Replace(" W", "").Trim()); if (isCPUPowerControlSupported && value >= 10 && value <= 254) { @@ -349,6 +354,216 @@ static void RestoreCPUPower() { } } + static bool TryGetCurrentCpuPowerSetting(out int value) { + value = -1; + if (cpuPower == "max") { + value = 254; + return true; + } + if (!string.IsNullOrEmpty(cpuPower) && cpuPower.Contains(" W")) { + int parsed; + if (int.TryParse(cpuPower.Replace(" W", "").Trim(), out parsed) && parsed >= 10 && parsed <= 254) { + value = parsed; + return true; + } + } + return false; + } + + static void LoadFanLockConfig(RegistryKey key) { + if (key == null) return; + fanLockMinimumPower = Math.Max(10, Math.Min(254, Convert.ToInt32(key.GetValue("FanLockMinimumPower", 20)))); + fanLockMaximumPower = Math.Max(fanLockMinimumPower, Math.Min(254, Convert.ToInt32(key.GetValue("FanLockMaximumPower", 120)))); + fanLockTargetRpm = Math.Max(0, Convert.ToInt32(key.GetValue("FanLockTargetRpm", 3000))) / 100 * 100; + fanLockTargetTemperature = Math.Max(40, Convert.ToInt32(key.GetValue("FanLockTargetTemperature", 80))); + fanLockReturnPreset = (string)key.GetValue("FanLockReturnPreset", "PresetCustom1"); + if (fanLockReturnPreset == FanLockPresetKey || !PresetOrder.Contains(fanLockReturnPreset)) + fanLockReturnPreset = "PresetCustom1"; + if (fanLockCurrentPower < 0) { + int savedStartPower = Convert.ToInt32(key.GetValue("FanLockStartPower", -1)); + if (savedStartPower >= 10) + fanLockCurrentPower = Math.Max(fanLockMinimumPower, Math.Min(fanLockMaximumPower, savedStartPower)); + } + } + + static void SaveFanLockConfig() { + try { + using (RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\OmenSuperHub")) { + if (key == null) return; + key.SetValue("FanLockMinimumPower", fanLockMinimumPower); + key.SetValue("FanLockMaximumPower", fanLockMaximumPower); + key.SetValue("FanLockTargetRpm", fanLockTargetRpm); + key.SetValue("FanLockTargetTemperature", fanLockTargetTemperature); + key.SetValue("FanLockReturnPreset", fanLockReturnPreset); + if (fanLockCurrentPower >= 10) + key.SetValue("FanLockStartPower", fanLockCurrentPower); + } + } catch (Exception ex) { + Logger.Error($"SaveFanLockConfig: {ex.Message}"); + } + } + + static bool ActivateFanLockPreset() { + if (!isCPUPowerControlSupported) return false; + + if (currentPreset != FanLockPresetKey) { + int startingPower; + if (!TryGetCurrentCpuPowerSetting(out startingPower)) { + MessageBox.Show( + Application.OpenForms.OfType().FirstOrDefault(), + Strings.FanLockNeedsCpuPower, + Strings.Hint, + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return false; + } + + fanLockReturnPreset = currentPreset; + fanLockCurrentPower = Math.Max(fanLockMinimumPower, Math.Min(fanLockMaximumPower, startingPower)); + SavePresetToRegistry(FanLockPresetKey); + currentPreset = FanLockPresetKey; + SaveConfig("CurrentPreset"); + SaveFanLockConfig(); + } + + LoadMonitorMetricSettings(FanLockPresetKey); + UpdateMonitorMetricCheckedStates(); + ApplyFanLockTargets(); + UpdateTrayIconText(); + UpdateFloatingText(); + return true; + } + + static void EnsureCpuMonitorForFanLock() { + if (monitorCPU && hwMonitorProcess != null && !hwMonitorProcess.HasExited) return; + monitorCPU = true; + cpuTempReady = false; + if (hwMonitorProcess == null || hwMonitorProcess.HasExited) + StartHardwareMonitor(); + else + SetCpuMonitorState(true); + UpdateCheckedState("monitorCPUGroup", Strings.MonitorCpuOn); + } + + static void ApplyFanLockTargets() { + if (currentPreset != FanLockPresetKey) return; + EnsureCpuMonitorForFanLock(); + + int maximumRpm = platformMaxFanSpeed.HasValue && platformMaxFanSpeed.Value > 0 + ? (int)(platformMaxFanSpeed.Value * 1.1) + : 6400; + fanLockTargetRpm = Math.Max(0, Math.Min(maximumRpm, fanLockTargetRpm)); + fanLockTargetRpm = fanLockTargetRpm / 100 * 100; + fanLockTargetTemperature = Math.Max(40, Math.Min((maxCPUTemp ?? 97) - 3, fanLockTargetTemperature)); + fanLockCurrentPower = Math.Max(fanLockMinimumPower, Math.Min(fanLockMaximumPower, fanLockCurrentPower)); + + fanControl = fanLockTargetRpm + " RPM"; + SetMaxFanSpeedOff(); + fanControlTimer.Change(Timeout.Infinite, Timeout.Infinite); + SetFanLevel(fanLockTargetRpm / 100, fanLockTargetRpm / 100, Is3FanNb); + SetCpuPowerLimit((byte)fanLockCurrentPower); + SetFanLockControlledMenusEnabled(false); + UpdateCheckedState("fanControlGroup", Strings.SetFanSpeedSlider); + fanLockUpdatingControls = true; + try { + if (fanTrackBar != null) + fanTrackBar.Value = Math.Max(fanTrackBar.Minimum, Math.Min(fanTrackBar.Maximum, fanLockTargetRpm / 100)); + if (cpuPowerTrackBar != null) + cpuPowerTrackBar.Value = Math.Max(cpuPowerTrackBar.Minimum, Math.Min(cpuPowerTrackBar.Maximum, fanLockCurrentPower)); + } finally { + fanLockUpdatingControls = false; + } + if (fanValueLabel != null) + fanValueLabel.Text = string.Format(Strings.CurrentSliderValueTemp, $"{fanLockTargetRpm} RPM"); + if (cpuPowerValueLabel != null) + cpuPowerValueLabel.Text = string.Format(Strings.CurrentSliderValueTemp, $"{fanLockCurrentPower} W"); + } + + static void AdjustFanLockPower() { + if (currentPreset != FanLockPresetKey || !monitorCPU || !cpuTempReady || fanLockCurrentPower < 0) + return; + + int nextPower = fanLockCurrentPower; + if (smoothedCPUTemp > fanLockTargetTemperature + 1f) + nextPower--; + else if (smoothedCPUTemp < fanLockTargetTemperature - 1f) + nextPower++; + + nextPower = Math.Max(fanLockMinimumPower, Math.Min(fanLockMaximumPower, nextPower)); + if (nextPower == fanLockCurrentPower) return; + + fanLockCurrentPower = nextPower; + SetCpuPowerLimit((byte)fanLockCurrentPower); + if (cpuPowerValueLabel != null && uiContext != null) { + uiContext.Post(_ => { + fanLockUpdatingControls = true; + try { + if (cpuPowerTrackBar != null) + cpuPowerTrackBar.Value = Math.Max(cpuPowerTrackBar.Minimum, Math.Min(cpuPowerTrackBar.Maximum, fanLockCurrentPower)); + } finally { + fanLockUpdatingControls = false; + } + if (cpuPowerValueLabel != null) + cpuPowerValueLabel.Text = string.Format(Strings.CurrentSliderValueTemp, $"{fanLockCurrentPower} W"); + }, null); + } + } + + static void LeaveFanLockPresetForSafety() { + if (currentPreset != FanLockPresetKey) return; + string targetPreset = fanLockReturnPreset; + if (targetPreset == FanLockPresetKey || !PresetOrder.Contains(targetPreset)) + targetPreset = "PresetCustom1"; + // 使用进入模式时保存的快照恢复全部设置,避免内置预设重新生成默认值。 + LoadPresetFields(FanLockPresetKey); + LoadMonitorMetricSettings(FanLockPresetKey); + currentPreset = targetPreset; + SaveConfig("CurrentPreset"); + SetFanLockControlledMenusEnabled(true); + + var item = FindMenuItemByName(trayIcon.ContextMenuStrip.Items, currentPreset); + if (item != null) + UpdateCheckedState("presetsGroup", null, item); + ApplyPresetSettings(FanLockPresetKey); + UpdateMonitorMetricCheckedStates(); + UpdateTrayIconText(); + UpdateFloatingText(); + } + + // 返回 true 表示功耗已经处于下限,可以进入风扇安全切换阶段。 + static bool PrepareFanLockOverheatProtection() { + if (currentPreset != FanLockPresetKey || fanLockCurrentPower <= fanLockMinimumPower) + return true; + + int distanceToMinimum = fanLockCurrentPower - fanLockMinimumPower; + int reduction = Math.Max(5, (distanceToMinimum + 1) / 2); + fanLockCurrentPower = Math.Max(fanLockMinimumPower, fanLockCurrentPower - reduction); + SetCpuPowerLimit((byte)fanLockCurrentPower); + + if (uiContext != null) { + uiContext.Post(_ => { + fanLockUpdatingControls = true; + try { + if (cpuPowerTrackBar != null) + cpuPowerTrackBar.Value = Math.Max(cpuPowerTrackBar.Minimum, Math.Min(cpuPowerTrackBar.Maximum, fanLockCurrentPower)); + } finally { + fanLockUpdatingControls = false; + } + if (cpuPowerValueLabel != null) + cpuPowerValueLabel.Text = string.Format(Strings.CurrentSliderValueTemp, $"{fanLockCurrentPower} W"); + }, null); + } + return false; + } + + static void SetFanLockControlledMenusEnabled(bool enabled) { + if (trayIcon == null || trayIcon.ContextMenuStrip == null) return; + var fanMenu = FindMenuItemByName(trayIcon.ContextMenuStrip.Items, "FanControlMenu"); + var cpuPowerMenu = FindMenuItemByName(trayIcon.ContextMenuStrip.Items, "CpuPowerMenu"); + if (fanMenu != null) fanMenu.Enabled = enabled; + if (cpuPowerMenu != null) cpuPowerMenu.Enabled = enabled; + } + static void RestorePowerConfig() { SetUnleashMode(); System.Threading.Tasks.Task.Delay(1000).ContinueWith(_ => { @@ -675,12 +890,6 @@ static bool IsMonitorMetricConfig(string configName) { static void SaveConfig(string configName = null) { // 内置预设下调整设置时,不再强制切换到 Custom1,直接保存注册表(不关联任何预设子键) try { - // 六项监控显示开关在自定义预设下只写入当前预设,避免污染内置预设的全局值。 - if (!IsBuiltInPreset(currentPreset) && IsMonitorMetricConfig(configName)) { - SavePresetToRegistry(currentPreset); - return; - } - using (RegistryKey key = Registry.CurrentUser.CreateSubKey(@"Software\OmenSuperHub")) { if (key != null) { if (configName == null) { @@ -716,14 +925,12 @@ static void SaveConfig(string configName = null) { key.SetValue("MonitorFan", monitorFan); key.SetValue("MonitorRefreshRate", monitorRefreshRate); key.SetValue("TempDisplayMode", tempDisplayMode); - if (IsBuiltInPreset(currentPreset)) { - key.SetValue("ShowCPUTemp", showCPUTemp); - key.SetValue("ShowCPUPower", showCPUPower); - key.SetValue("ShowCPUFrequency", showCPUFrequency); - key.SetValue("ShowGPUTemp", showGPUTemp); - key.SetValue("ShowGPUPower", showGPUPower); - key.SetValue("ShowGPUFrequency", showGPUFrequency); - } + key.SetValue("ShowCPUTemp", showCPUTemp); + key.SetValue("ShowCPUPower", showCPUPower); + key.SetValue("ShowCPUFrequency", showCPUFrequency); + key.SetValue("ShowGPUTemp", showGPUTemp); + key.SetValue("ShowGPUPower", showGPUPower); + key.SetValue("ShowGPUFrequency", showGPUFrequency); key.SetValue("FloatingBarLoc", floatingBarLoc); key.SetValue("FloatingBar", floatingBar); key.SetValue("FloatingBarScreen", floatingBarScreen); @@ -956,6 +1163,11 @@ static void LoadMonitorMetricSettings(string presetKey) { try { using (RegistryKey globalKey = Registry.CurrentUser.OpenSubKey(@"Software\OmenSuperHub")) { if (globalKey != null) { + monitorCPU = Convert.ToBoolean(globalKey.GetValue("MonitorCPU", true)); + monitorGPU = hasNVIDIAGpu && Convert.ToBoolean(globalKey.GetValue("MonitorGPU", true)); + monitorFan = Convert.ToBoolean(globalKey.GetValue("MonitorFan", false)); + monitorRefreshRate = (string)globalKey.GetValue("MonitorRefreshRate", "low"); + tempDisplayMode = (string)globalKey.GetValue("TempDisplayMode", "smoothed"); globalShowCPUTemp = Convert.ToBoolean(globalKey.GetValue("ShowCPUTemp", true)); globalShowCPUPower = Convert.ToBoolean(globalKey.GetValue("ShowCPUPower", true)); globalShowCPUFrequency = Convert.ToBoolean(globalKey.GetValue("ShowCPUFrequency", false)); @@ -965,24 +1177,12 @@ static void LoadMonitorMetricSettings(string presetKey) { } } - if (IsBuiltInPreset(presetKey)) { - showCPUTemp = globalShowCPUTemp; - showCPUPower = globalShowCPUPower; - showCPUFrequency = globalShowCPUFrequency; - showGPUTemp = globalShowGPUTemp; - showGPUPower = globalShowGPUPower; - showGPUFrequency = globalShowGPUFrequency; - return; - } - - using (RegistryKey presetKeyHandle = Registry.CurrentUser.OpenSubKey($@"Software\OmenSuperHub\{presetKey}")) { - showCPUTemp = Convert.ToBoolean(presetKeyHandle?.GetValue("ShowCPUTemp", globalShowCPUTemp) ?? globalShowCPUTemp); - showCPUPower = Convert.ToBoolean(presetKeyHandle?.GetValue("ShowCPUPower", globalShowCPUPower) ?? globalShowCPUPower); - showCPUFrequency = Convert.ToBoolean(presetKeyHandle?.GetValue("ShowCPUFrequency", globalShowCPUFrequency) ?? globalShowCPUFrequency); - showGPUTemp = Convert.ToBoolean(presetKeyHandle?.GetValue("ShowGPUTemp", globalShowGPUTemp) ?? globalShowGPUTemp); - showGPUPower = Convert.ToBoolean(presetKeyHandle?.GetValue("ShowGPUPower", globalShowGPUPower) ?? globalShowGPUPower); - showGPUFrequency = Convert.ToBoolean(presetKeyHandle?.GetValue("ShowGPUFrequency", globalShowGPUFrequency) ?? globalShowGPUFrequency); - } + showCPUTemp = globalShowCPUTemp; + showCPUPower = globalShowCPUPower; + showCPUFrequency = globalShowCPUFrequency; + showGPUTemp = globalShowGPUTemp; + showGPUPower = globalShowGPUPower; + showGPUFrequency = globalShowGPUFrequency; } catch (Exception ex) { Logger.Error($"LoadMonitorMetricSettings({presetKey}): {ex.Message}"); } @@ -1003,7 +1203,7 @@ static void UpdateMonitorMetricCheckedStates() { /// static void ApplyPresetSettings(string presetKey) { // 自定义预设特有字段:监控项、温度显示模式等 - if (presetKey == "Restore" || presetKey == "PresetCustom1" || presetKey == "PresetCustom2" || presetKey == "PresetCustom3") { + if (presetKey == "Restore" || presetKey == FanLockPresetKey || presetKey == "PresetCustom1" || presetKey == "PresetCustom2" || presetKey == "PresetCustom3") { if (presetKey == "Restore") { try { using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"Software\OmenSuperHub")) { @@ -1036,9 +1236,13 @@ static void ApplyPresetSettings(string presetKey) { if (!wasMonitorRunning) { cpuTempReady = gpuTempReady = tempReady = false; StartHardwareMonitor(); + } else if (!monitorGPU) { + gpuTempReady = false; + rawPowerGPU = rawFrequencyGPU = GPUPower = GPUFrequency = 0f; + StopHardwareMonitorAndWait(5000); + if (monitorCPU) StartHardwareMonitor(); } else { if (!monitorCPU) { cpuTempReady = false; rawPowerCPU = 0f; CPUPower = 0f; } - if (!monitorGPU) { gpuTempReady = false; rawPowerGPU = 0f; GPUPower = 0f; } SetCpuMonitorState(monitorCPU); SetGpuMonitorState(monitorGPU); } @@ -1077,22 +1281,24 @@ static void ApplyPresetSettings(string presetKey) { UpdateCheckedState("fanTableGroup", Strings.FanCustomMode); } - // 风扇控制模式 - if (fanControl == "auto") { - SetMaxFanSpeedOff(); - fanControlTimer.Change(0, 1000); - UpdateCheckedState("fanControlGroup", Strings.FanAuto); - } else if (fanControl.Contains("max")) { - SetMaxFanSpeedOn(); - fanControlTimer.Change(Timeout.Infinite, Timeout.Infinite); - UpdateCheckedState("fanControlGroup", Strings.FanMax); - } else if (fanControl.Contains(" RPM")) { - SetMaxFanSpeedOff(); - fanControlTimer.Change(Timeout.Infinite, Timeout.Infinite); - int rpmValue = int.Parse(fanControl.Replace(" RPM", "").Trim()); - SetFanLevel(rpmValue / 100, rpmValue / 100, Is3FanNb); - if (fanTrackBar != null) fanTrackBar.Value = rpmValue / 100; - UpdateCheckedState("fanControlGroup", Strings.SetFanSpeedSlider); + // 风扇控制模式;锁定风扇预设在本方法末尾单独应用。 + if (currentPreset != FanLockPresetKey) { + if (fanControl == "auto") { + SetMaxFanSpeedOff(); + fanControlTimer.Change(0, 1000); + UpdateCheckedState("fanControlGroup", Strings.FanAuto); + } else if (fanControl.Contains("max")) { + SetMaxFanSpeedOn(); + fanControlTimer.Change(Timeout.Infinite, Timeout.Infinite); + UpdateCheckedState("fanControlGroup", Strings.FanMax); + } else if (fanControl.Contains(" RPM")) { + SetMaxFanSpeedOff(); + fanControlTimer.Change(Timeout.Infinite, Timeout.Infinite); + int rpmValue = int.Parse(fanControl.Replace(" RPM", "").Trim()); + SetFanLevel(rpmValue / 100, rpmValue / 100, Is3FanNb); + if (fanTrackBar != null) fanTrackBar.Value = rpmValue / 100; + UpdateCheckedState("fanControlGroup", Strings.SetFanSpeedSlider); + } } // 风扇响应速度 @@ -1104,7 +1310,7 @@ static void ApplyPresetSettings(string presetKey) { } // CPU 功耗 - if (isCPUPowerControlSupported) { + if (isCPUPowerControlSupported && currentPreset != FanLockPresetKey) { if (cpuPower == "null") { UpdateCheckedState("cpuPowerGroup", Strings.NotSet); } else if (cpuPower == "max") { @@ -1214,6 +1420,9 @@ static void ApplyPresetSettings(string presetKey) { } } }); + + if (currentPreset == FanLockPresetKey) + ApplyFanLockTargets(); } /// @@ -1221,6 +1430,11 @@ static void ApplyPresetSettings(string presetKey) { /// 保存到注册表,然后应用到硬件。 /// static void applyPresetLogic(string targetPreset) { + if (targetPreset == FanLockPresetKey) { + ActivateFanLockPreset(); + return; + } + SetFanLockControlledMenusEnabled(true); currentPreset = targetPreset; if (targetPreset == "PresetExtreme" || targetPreset == "PresetGpuPriority" || targetPreset == "PresetLightUse") { @@ -1289,6 +1503,7 @@ static void RestoreConfig() { presetCustom1Name = (string)key.GetValue("PresetCustom1Name", Strings.PresetCustom1); presetCustom2Name = (string)key.GetValue("PresetCustom2Name", Strings.PresetCustom2); presetCustom3Name = (string)key.GetValue("PresetCustom3Name", Strings.PresetCustom3); + LoadFanLockConfig(key); // 旧版升级兼容:不存在 CurrentPreset 键时迁移 if (key.GetValue("CurrentPreset") == null) { @@ -1337,6 +1552,13 @@ static void RestoreConfig() { } else { LoadPresetFields(currentPreset); } + if (currentPreset == FanLockPresetKey && fanLockCurrentPower < 10) { + int restoredPower; + if (TryGetCurrentCpuPowerSetting(out restoredPower)) + fanLockCurrentPower = Math.Max(fanLockMinimumPower, Math.Min(fanLockMaximumPower, restoredPower)); + else + fanLockCurrentPower = fanLockMinimumPower; + } LoadMonitorMetricSettings(currentPreset); var item = FindMenuItemByName(trayIcon.ContextMenuStrip.Items, currentPreset); @@ -1438,6 +1660,8 @@ static void RestoreConfig() { /// static void SavePresetToRegistry(string presetKey) { if (presetKey == "PresetExtreme" || presetKey == "PresetGpuPriority" || presetKey == "PresetLightUse") return; + // 此子键在进入锁定模式前保存完整快照;模式运行期间不得被后续菜单修改覆盖。 + if (presetKey == FanLockPresetKey && currentPreset == FanLockPresetKey) return; try { using (RegistryKey key = Registry.CurrentUser.CreateSubKey($@"Software\OmenSuperHub\{presetKey}")) { if (key == null) return; diff --git a/Program.GpuSleep.cs b/Program.GpuSleep.cs new file mode 100644 index 0000000..f72c485 --- /dev/null +++ b/Program.GpuSleep.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using static OmenSuperHub.GpuAppManager; + +namespace OmenSuperHub { + static partial class Program { + static bool gpuSleepOperationRunning; + static volatile bool gpuSleepGuardActive; + + static bool ShouldBlockBackgroundGpuQueries { + get { return gpuSleepGuardActive || !monitorGPU; } + } + + static async void StartGpuSleepAttempt() { + if (gpuSleepOperationRunning) return; + if (string.Equals(NvGraphicsMode.ToString(), "Discrete", StringComparison.OrdinalIgnoreCase)) { + MessageBox.Show(Strings.GpuSleepDiscreteModeBlocked, Strings.Warning, MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + if (!GpuSleepManager.TryGetNvidiaSleepTarget(out GpuSleepManager.GpuDeviceInfo target, out string reason)) { + MessageBox.Show(reason, Strings.Warning, MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + if (MessageBox.Show(Strings.GpuSleepConfirm, Strings.GpuSleepMenu, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) != DialogResult.Yes) return; + + gpuSleepOperationRunning = true; + bool oldMonitorGpu = monitorGPU; + bool oldSleepGuard = gpuSleepGuardActive; + try { + await DisableGpuMonitoringForSleep(); + bool slept = await RestartAndWaitForD3(target); + if (slept) { FinishGpuSleepSuccess(); return; } + + List processes = await Task.Run(() => GpuSleepManager.GetNvidiaProcesses()); + if (!processes.Any(p => p.CanConfigurePreference)) { + MessageBox.Show(Strings.GpuSleepFailedNoProcesses, Strings.GpuSleepMenu, MessageBoxButtons.OK, MessageBoxIcon.Information); + RestoreGpuMonitoringAfterCancelledAttempt(oldMonitorGpu, oldSleepGuard); + return; + } + + using (var repair = new GpuSleepRepairForm(processes)) { + if (repair.ShowDialog() != DialogResult.OK) { + RestoreGpuMonitoringAfterCancelledAttempt(oldMonitorGpu, oldSleepGuard); + return; + } + } + + slept = await RestartAndWaitForD3(target); + if (slept) FinishGpuSleepSuccess(); + else { + List remaining = await Task.Run(() => GpuSleepManager.GetNvidiaProcesses()); + string details = remaining.Count == 0 ? "" : Environment.NewLine + Environment.NewLine + string.Join(Environment.NewLine, remaining.Select(p => $"{p.ProcessName} (PID {p.ProcessId}, {p.UsageType})")); + MessageBox.Show(Strings.GpuSleepStillFailed + details, Strings.GpuSleepMenu, MessageBoxButtons.OK, MessageBoxIcon.Information); + RestoreGpuMonitoringAfterCancelledAttempt(oldMonitorGpu, oldSleepGuard); + } + } catch (Exception ex) { + Logger.Error("GPU sleep attempt failed: " + ex); + MessageBox.Show(ex.Message, Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + RestoreGpuMonitoringAfterCancelledAttempt(oldMonitorGpu, oldSleepGuard); + } finally { + gpuSleepOperationRunning = false; + } + } + + static async Task DisableGpuMonitoringForSleep() { + gpuSleepGuardActive = true; + monitorGPU = false; + gpuTempReady = false; + rawGotGPU = false; + rawPowerGPU = rawFrequencyGPU = GPUPower = GPUFrequency = 0f; + bool stopped = await Task.Run(() => StopHardwareMonitorAndWait(5000)); + if (!stopped) throw new InvalidOperationException(Strings.GpuSleepMonitorStopFailed); + if (monitorCPU) StartHardwareMonitor(); + UpdateCheckedState("monitorGPUGroup", Strings.MonitorGpuOff); + } + + static async Task RestartAndWaitForD3(GpuSleepManager.GpuDeviceInfo target) { + ProcessResult result = await Task.Run(() => RestartGpu(target.InstanceId)); + if (result.ExitCode != 0) throw new InvalidOperationException(Strings.GpuSleepRestartFailed(result.Error)); + await Task.Delay(3000); + for (int i = 0; i < 8; i++) { + GpuSleepManager.DevicePowerState state = await Task.Run(() => GpuSleepManager.GetDevicePowerState(target.InstanceId)); + if (state == GpuSleepManager.DevicePowerState.D3) return true; + await Task.Delay(2000); + } + return false; + } + + static void FinishGpuSleepSuccess() { + SaveConfig("MonitorGPU"); + MessageBox.Show(Strings.GpuSleepSucceeded, Strings.GpuSleepMenu, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + static void RestoreGpuMonitoringAfterCancelledAttempt(bool oldValue, bool oldGuardValue) { + gpuSleepGuardActive = oldGuardValue; + if (!oldValue) return; + monitorGPU = true; + gpuTempReady = false; + SetGpuMonitorState(true); + UpdateCheckedState("monitorGPUGroup", Strings.MonitorGpuOn); + } + + static void RestoreLatestGpuPreferences() { + string id = GpuSleepManager.GetLatestTransactionId(); + if (string.IsNullOrEmpty(id)) { MessageBox.Show(Strings.GpuPreferenceBackupMissing, Strings.Hint); return; } + GpuSleepManager.PreferenceTransaction transaction = GpuSleepManager.LoadTransaction(id); + GpuSleepManager.RestoreResult result = GpuSleepManager.RestorePreferences(id, false); + if (result.ConflictedPaths.Count > 0 && MessageBox.Show(Strings.GpuPreferenceConflict, Strings.Warning, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) + result = GpuSleepManager.RestorePreferences(id, true); + if (!result.Success) { MessageBox.Show(result.Error ?? Strings.GpuPreferenceRestoreFailed, Strings.Error); return; } + MessageBox.Show(Strings.GpuPreferenceRestoredRestartRequired, Strings.Hint); + if (transaction != null && MessageBox.Show(Strings.GpuSleepRestartWarning, Strings.Warning, MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes) { + var apps = transaction.Entries.Select(e => new GpuSleepManager.GpuProcessInfo { ProcessName = e.ProcessName, ExecutablePath = e.ExecutablePath }).ToList(); + GpuSleepManager.RestartApplications(apps, true); + } + } + } +} diff --git a/Program.Menu.cs b/Program.Menu.cs index 86bd8c9..bf7a0f1 100644 --- a/Program.Menu.cs +++ b/Program.Menu.cs @@ -105,9 +105,13 @@ static void BuildTrayMenu(ContextMenuStrip menu) { gpuPowerLimitsMenu = new ToolStripMenuItem($"{Strings.SysNvidiaPower}: --W / --W") { Enabled = false }; sysInfoMenu.DropDownItems.Add(gpuPowerLimitsMenu); System.Threading.Tasks.Task.Run(() => { - string gpuModel = GetGpuModelFromNvidiaSmi(); - var limits = GetGpuPowerLimits(); - string limitsText = limits[0] == -2f ? "--W / --W" : $"{limits[0]:F0}W / {limits[1]:F0}W"; + string gpuModel = GetNvidiaGpuInfoList().FirstOrDefault().Name ?? Strings.GpuSleepTargetNotFound; + string limitsText = "--W / --W"; + if (!ShouldBlockBackgroundGpuQueries) { + gpuModel = GetGpuModelFromNvidiaSmi(); + var limits = GetGpuPowerLimits(); + limitsText = limits[0] == -2f ? "--W / --W" : $"{limits[0]:F0}W / {limits[1]:F0}W"; + } Thread.Sleep(2000); uiContext.Post(_ => { gpuPowerLimitsMenu.Text = $"{Strings.SysNvidiaPower}: {limitsText}"; @@ -144,7 +148,7 @@ static void BuildTrayMenu(ContextMenuStrip menu) { // 订阅 DropDownOpening 和 DropDownClosed 事件来控制是否更新信息 sysInfoMenu.DropDownOpening += (s, e) => { - if (hasNVIDIAGpu) { + if (hasNVIDIAGpu && !ShouldBlockBackgroundGpuQueries) { System.Threading.Tasks.Task.Run(() => { var limits = GetGpuPowerLimits(); string limitsText = limits[0] == -2f ? "--W / --W" : $"{limits[0]:F0}W / {limits[1]:F0}W"; @@ -194,6 +198,23 @@ static void BuildTrayMenu(ContextMenuStrip menu) { lightUseItem.Name = "PresetLightUse"; presetsMenu.DropDownItems.Add(lightUseItem); presetsMenu.DropDownItems.Add(new ToolStripSeparator()); + + var fanLockItem = new ToolStripMenuItem(Strings.PresetFanLock) { + Name = FanLockPresetKey, + Tag = "presetsGroup", + Checked = currentPreset == FanLockPresetKey, + ToolTipText = Strings.PresetFanLockTooltip + }; + fanLockItem.MouseUp += (s, e) => { + if (e.Button == MouseButtons.Left) { + if (ActivateFanLockPreset()) + UpdateCheckedState("presetsGroup", null, fanLockItem); + } else if (e.Button == MouseButtons.Right) { + ShowFanLockEditor(); + } + }; + presetsMenu.DropDownItems.Add(fanLockItem); + presetsMenu.DropDownItems.Add(new ToolStripSeparator()); } var custom1Item = CreateMenuItem(presetCustom1Name, "presetsGroup", (s, e) => applyPresetLogic("PresetCustom1"), currentPreset == "PresetCustom1"); @@ -344,7 +365,7 @@ void attachRename(ToolStripMenuItem item, string presetKey) { menu.Items.Add(fanConfigMenu); - ToolStripMenuItem fanControlMenu = new ToolStripMenuItem(Strings.FanControl); + ToolStripMenuItem fanControlMenu = new ToolStripMenuItem(Strings.FanControl) { Name = "FanControlMenu" }; if (isFanCleanSupported || isFanLegacyCleanSupported) { string menuText = Strings.CleanCreekMenuItem; if (!isFanCleanSupported && isFanLegacyCleanSupported) @@ -403,6 +424,7 @@ void attachRename(ToolStripMenuItem item, string presetKey) { fanValueLabel = new ToolStripMenuItem(string.Format(Strings.CurrentSliderValueTemp, $"{fanTrackBar.Value * 100} RPM")) { Enabled = false }; fanTrackBar.ValueChanged += (sender, e) => { + if (fanLockUpdatingControls) return; fanControl = fanTrackBar.Value * 100 + " RPM"; fanValueLabel.Text = string.Format(Strings.CurrentSliderValueTemp, $"{fanTrackBar.Value * 100} RPM"); fanControlTimer.Change(Timeout.Infinite, Timeout.Infinite); @@ -514,24 +536,14 @@ void attachRename(ToolStripMenuItem item, string presetKey) { ToolStripMenuItem gpuAppsMenu = new ToolStripMenuItem(Strings.GpuAppsMenu); gpuAppsMenu.DropDownOpening += (s, e) => { gpuAppsMenu.DropDownItems.Clear(); - var apps = GetGpuApps(); - if (apps.Count == 0) { - gpuAppsMenu.DropDownItems.Add(new ToolStripMenuItem(Strings.GpuAppsNone) { Enabled = false }); - } else { - foreach (var app in apps) { - var appItem = new ToolStripMenuItem($"{app.ProcessName} (PID: {app.ProcessId})"); - appItem.Click += (sender, args) => { - if (MessageBox.Show(Application.OpenForms.OfType().FirstOrDefault(), Strings.GpuCloseConfirm(app.ProcessName), Strings.GpuCloseTitle, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { - try { - Process.GetProcessById(app.ProcessId).Kill(); - } catch (Exception ex) { - MessageBox.Show(Application.OpenForms.OfType().FirstOrDefault(), Strings.GpuCloseError(ex.Message), Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - }; - gpuAppsMenu.DropDownItems.Add(appItem); - } + if (ShouldBlockBackgroundGpuQueries) { + gpuAppsMenu.DropDownItems.Add(new ToolStripMenuItem(Strings.GpuAppsQueryMayWake) { Enabled = false }); + var queryItem = new ToolStripMenuItem(Strings.GpuAppsQueryNow); + queryItem.Click += (sender, args) => PopulateGpuAppsMenu(gpuAppsMenu); + gpuAppsMenu.DropDownItems.Add(queryItem); + return; } + PopulateGpuAppsMenu(gpuAppsMenu); }; performanceControlMenu.DropDownItems.Add(gpuAppsMenu); @@ -543,6 +555,16 @@ void attachRename(ToolStripMenuItem item, string presetKey) { } }; performanceControlMenu.DropDownItems.Add(restartGpuMenu); + + ToolStripMenuItem gpuSleepMenu = new ToolStripMenuItem(Strings.GpuSleepMenu); + gpuSleepMenu.ToolTipText = Strings.GpuSleepTooltip; + gpuSleepMenu.Click += (s, e) => StartGpuSleepAttempt(); + performanceControlMenu.DropDownItems.Add(gpuSleepMenu); + + ToolStripMenuItem restoreGpuPreferencesMenu = new ToolStripMenuItem(Strings.GpuPreferenceRestore); + restoreGpuPreferencesMenu.Enabled = !string.IsNullOrEmpty(GpuSleepManager.GetLatestTransactionId()); + restoreGpuPreferencesMenu.Click += (s, e) => RestoreLatestGpuPreferences(); + performanceControlMenu.DropDownItems.Add(restoreGpuPreferencesMenu); } performanceControlMenu.DropDownItems.Add(new ToolStripSeparator()); // Separator between groups //ToolStripMenuItem pl4Menu = new ToolStripMenuItem("PL4"); @@ -609,7 +631,7 @@ void attachRename(ToolStripMenuItem item, string presetKey) { } if (isCPUPowerControlSupported) { - ToolStripMenuItem cpuPowerMenu = new ToolStripMenuItem(Strings.CpuPowerMenu); + ToolStripMenuItem cpuPowerMenu = new ToolStripMenuItem(Strings.CpuPowerMenu) { Name = "CpuPowerMenu" }; cpuPowerMenu.DropDownItems.Add(new ToolStripMenuItem(Strings.PerfCpuPowerTip) { Enabled = false }); cpuPowerMenu.DropDownItems.Add(new ToolStripSeparator()); @@ -638,6 +660,7 @@ void attachRename(ToolStripMenuItem item, string presetKey) { // 滑块值改变时更新标签并应用设置 cpuPowerTrackBar.ValueChanged += (sender, e) => { + if (fanLockUpdatingControls) return; int val = cpuPowerTrackBar.Value; cpuPowerValueLabel.Text = string.Format(Strings.CurrentSliderValueTemp, $"{val} W"); cpuPower = cpuPowerTrackBar.Value + " W"; @@ -1092,17 +1115,13 @@ ToolStripMenuItem CreateMonitorMetricItem(string text, string group, Func ToolStripMenuItem monitorGPUMenu = new ToolStripMenuItem(Strings.MonitorGpuLabel); monitorGPUMenu.DropDownItems.Add(CreateMenuItem(Strings.MonitorGpuOn, "monitorGPUGroup", (s, e) => { bool wasAllOff = !monitorCPU && !monitorGPU; + gpuSleepGuardActive = false; monitorGPU = true; gpuTempReady = false; // 等待获取到温度后再参与风扇控制 rawPowerGPU = 0f; // 清除可能残留的脏功率值 rawFrequencyGPU = 0f; GPUPower = 0f; GPUFrequency = 0f; - if (hasStopAuto) - autoStopMonitorGPU = false; - //重置自动开启标志 - hasStartAuto = false; - autoStartMonitorGPU = true; if (wasAllOff) { // 从全关状态重启监控进程 tempReady = false; @@ -1117,7 +1136,7 @@ ToolStripMenuItem CreateMonitorMetricItem(string text, string group, Func } SaveConfig("MonitorGPU"); }, true)); - monitorGPUMenu.DropDownItems.Add(CreateMenuItem(Strings.MonitorGpuOff, "monitorGPUGroup", (s, e) => { + monitorGPUMenu.DropDownItems.Add(CreateMenuItem(Strings.MonitorGpuOff, "monitorGPUGroup", async (s, e) => { // 自动转速模式下禁止彻底关闭监控 if (!monitorCPU && fanControl == "auto") { MessageBox.Show(Application.OpenForms.OfType().FirstOrDefault(), Strings.MonitorAutoFanWarning, Strings.Hint, MessageBoxButtons.OK, MessageBoxIcon.Warning); @@ -1130,17 +1149,17 @@ ToolStripMenuItem CreateMonitorMetricItem(string text, string group, Func rawPowerGPU = 0f; // 关闭时清零,避免重新开启时读到旧值 rawFrequencyGPU = 0f; GPUPower = 0f; - GPUFrequency = 0f; - if (hasStartAuto) - autoStartMonitorGPU = false; - //重置自动关闭标志 - hasStopAuto = false; - autoStopMonitorGPU = true; - SetGpuMonitorState(false); - // 若CPU和GPU均已关闭,停止监控进程 - if (!monitorCPU && !monitorGPU) { - StopHardwareMonitor(); +GPUFrequency = 0f; + bool stopped = await Task.Run(() => StopHardwareMonitorAndWait(5000)); + if (!stopped) { + monitorGPU = true; + SetGpuMonitorState(true); + UpdateCheckedState("monitorGPUGroup", Strings.MonitorGpuOn); + MessageBox.Show(Strings.MonitorGpuStopFailed, Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + SaveConfig("MonitorGPU"); + return; } + if (monitorCPU) StartHardwareMonitor(); SaveConfig("MonitorGPU"); }, false)); monitorGPUMenu.DropDownItems.Add(new ToolStripSeparator()); @@ -2166,6 +2185,85 @@ ex is UnauthorizedAccessException || } } + static void ShowFanLockEditor() { + int maximumRpm = platformMaxFanSpeed.HasValue && platformMaxFanSpeed.Value > 0 + ? (int)(platformMaxFanSpeed.Value * 1.1) + : 6400; + maximumRpm = Math.Max(100, maximumRpm / 100 * 100); + int maximumTemperature = Math.Max(40, (maxCPUTemp ?? 97) - 3); + + using (Form form = new Form { + Width = 430, + Height = 310, + FormBorderStyle = FormBorderStyle.FixedDialog, + MaximizeBox = false, + MinimizeBox = false, + StartPosition = FormStartPosition.CenterScreen, + Text = Strings.FanLockEditorTitle + }) { + Label minimumPowerLabel = new Label { Left = 15, Top = 22, Width = 230, Text = Strings.FanLockMinimumPower }; + NumericUpDown minimumPowerInput = new NumericUpDown { + Left = 270, Top = 18, Width = 120, Minimum = 10, Maximum = 254, + Value = Math.Max(10, Math.Min(254, fanLockMinimumPower)) + }; + + Label maximumPowerLabel = new Label { Left = 15, Top = 66, Width = 230, Text = Strings.FanLockMaximumPower }; + NumericUpDown maximumPowerInput = new NumericUpDown { + Left = 270, Top = 62, Width = 120, Minimum = 10, Maximum = 254, + Value = Math.Max(10, Math.Min(254, fanLockMaximumPower)) + }; + + minimumPowerInput.ValueChanged += (s, e) => { + if (minimumPowerInput.Value > maximumPowerInput.Value) + maximumPowerInput.Value = minimumPowerInput.Value; + }; + maximumPowerInput.ValueChanged += (s, e) => { + if (maximumPowerInput.Value < minimumPowerInput.Value) + minimumPowerInput.Value = maximumPowerInput.Value; + }; + + Label rpmLabel = new Label { Left = 15, Top = 110, Width = 230, Text = Strings.FanLockTargetRpm }; + NumericUpDown rpmInput = new NumericUpDown { + Left = 270, Top = 106, Width = 120, Minimum = 0, Maximum = maximumRpm, + Increment = 100, + Value = Math.Max(0, Math.Min(maximumRpm, fanLockTargetRpm)) + }; + + Label temperatureLabel = new Label { Left = 15, Top = 154, Width = 230, Text = Strings.FanLockTargetTemperature }; + NumericUpDown temperatureInput = new NumericUpDown { + Left = 270, Top = 150, Width = 120, Minimum = 40, Maximum = maximumTemperature, + Value = Math.Max(40, Math.Min(maximumTemperature, fanLockTargetTemperature)) + }; + + Button okButton = new Button { + Text = Strings.OK, Left = 110, Top = 205, Width = 95, + DialogResult = DialogResult.OK + }; + Button cancelButton = new Button { + Text = Strings.Cancel, Left = 220, Top = 205, Width = 95, + DialogResult = DialogResult.Cancel + }; + + form.Controls.AddRange(new Control[] { + minimumPowerLabel, minimumPowerInput, maximumPowerLabel, maximumPowerInput, + rpmLabel, rpmInput, temperatureLabel, temperatureInput, okButton, cancelButton + }); + form.AcceptButton = okButton; + form.CancelButton = cancelButton; + + if (form.ShowDialog() != DialogResult.OK) return; + + fanLockMinimumPower = (int)minimumPowerInput.Value; + fanLockMaximumPower = (int)maximumPowerInput.Value; + fanLockTargetRpm = (int)rpmInput.Value / 100 * 100; + fanLockTargetTemperature = (int)temperatureInput.Value; + SaveFanLockConfig(); + + if (currentPreset == FanLockPresetKey) + ApplyFanLockTargets(); + } + } + static bool ApplyCustomFanConfig() { string baseDirectory = AppDomain.CurrentDomain.BaseDirectory; @@ -2197,6 +2295,27 @@ ex is UnauthorizedAccessException || } } + static void PopulateGpuAppsMenu(ToolStripMenuItem gpuAppsMenu) { + gpuAppsMenu.DropDownItems.Clear(); + var apps = GpuSleepManager.GetNvidiaProcesses(); + if (apps.Count == 0) { + gpuAppsMenu.DropDownItems.Add(new ToolStripMenuItem(Strings.GpuAppsNone) { Enabled = false }); + return; + } + foreach (var app in apps) { + var appItem = new ToolStripMenuItem($"{app.ProcessName} (PID: {app.ProcessId})"); + appItem.Click += (sender, args) => { + if (MessageBox.Show(Application.OpenForms.OfType().FirstOrDefault(), Strings.GpuCloseConfirm(app.ProcessName), Strings.GpuCloseTitle, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; + try { + Process.GetProcessById(app.ProcessId).Kill(); + } catch (Exception ex) { + MessageBox.Show(Application.OpenForms.OfType().FirstOrDefault(), Strings.GpuCloseError(ex.Message), Strings.Error, MessageBoxButtons.OK, MessageBoxIcon.Error); + } + }; + gpuAppsMenu.DropDownItems.Add(appItem); + } + } + static ToolStripMenuItem CreateMenuItem(string text, string group, EventHandler action, bool isChecked, string toolTip = null) { var item = new ToolStripMenuItem(text) { Tag = group, diff --git a/Program.OmenKey.cs b/Program.OmenKey.cs index 00112e7..6e7bee7 100644 --- a/Program.OmenKey.cs +++ b/Program.OmenKey.cs @@ -253,6 +253,7 @@ static bool IsPresetAvailable(string presetKey) { case "PresetExtreme": case "PresetGpuPriority": case "PresetLightUse": + case FanLockPresetKey: return isCPUPowerControlSupported; case "PresetCustom1": case "PresetCustom2": @@ -268,7 +269,8 @@ static List GetAvailablePresetKeys() { } static string GetDefaultOmenKeyPresetCandidates() { - return string.Join(";", GetAvailablePresetKeys()); + // 锁定风扇模式会接管风扇与CPU功耗,不默认加入快捷键轮换,用户仍可手动勾选。 + return string.Join(";", GetAvailablePresetKeys().Where(key => key != FanLockPresetKey)); } static List GetOmenKeyPresetCandidateKeys() { diff --git a/Program.cs b/Program.cs index 469f29c..06b12d5 100644 --- a/Program.cs +++ b/Program.cs @@ -100,7 +100,8 @@ struct MSLLHOOKSTRUCT { static int textSize = 40; static int countRestore = 0, gpuClock = 0, gpuCoreOverclock = -1, gpuMemoryOverclock = -1, maxFrameRate = -1, graphicsBoostClock = 0; static int alreadyRead = 0, alreadyReadCode = 1000; - static readonly string[] PresetOrder = { "PresetExtreme", "PresetGpuPriority", "PresetLightUse", "PresetCustom1", "PresetCustom2", "PresetCustom3" }; + const string FanLockPresetKey = "PresetFanLock"; + static readonly string[] PresetOrder = { "PresetExtreme", "PresetGpuPriority", "PresetLightUse", FanLockPresetKey, "PresetCustom1", "PresetCustom2", "PresetCustom3" }; static string currentPreset = "PresetCustom1", presetCustom1Name = Strings.PresetCustom1, presetCustom2Name = Strings.PresetCustom2, presetCustom3Name = Strings.PresetCustom3; static string fanTable = "cool", fanControl = "auto", tempSensitivity = "high", tppPower = "null", iccMax = "null", acLoadline = "null", cpuPower = "null", tgpPower = "on", ppabPower = "on", dState = "normal", autoStart = "off", customIcon = "original", floatingBar = "off", floatingBarLoc = "left", floatingBarScreen = "", omenKey = OmenKeyActions.Default, omenKeyAppPath = "", omenKeyAppName = "", omenKeyShortcut = "", omenKeyPresetCandidates = "", dataLocalize = "off", appLanguage = "zh-CN", autoFanProtect = "on"; static volatile bool monitorFan = false; @@ -117,11 +118,16 @@ struct MSLLHOOKSTRUCT { static int? maxGPUTemp = null; static float CPUTemp = 50, GPUTemp = 40, rawTempCPU = 50f, rawTempGPU = 40f; static float CPUPower = 0, GPUPower = 0, CPUFrequency = 0f, GPUFrequency = 0f, rawPowerCPU = 0f, rawPowerGPU = 0f, rawFrequencyCPU = 0f, rawFrequencyGPU = 0f; + static int fanLockMinimumPower = 20, fanLockMaximumPower = 120, fanLockTargetRpm = 3000, fanLockTargetTemperature = 80; + static int fanLockCurrentPower = -1; + static string fanLockReturnPreset = "PresetCustom1"; + static bool fanLockUpdatingControls = false; static bool rawGotGPU = false; static volatile bool tempReady = false; // 子进程首次输出有效温度后置 true static volatile bool cpuTempReady = false; // CPU 温度已初始化给平滑值,允许参与风扇控制 static volatile bool gpuTempReady = false; // GPU 温度已初始化给平滑值,允许参与风扇控制 - static volatile bool hwMonitorStopping = false; // 主动停止时置 true,阻止 Exited 自动重启 + static readonly object hwMonitorLifecycleLock = new object(); + static readonly HashSet hwMonitorStoppingProcesses = new HashSet(); // 精确标记主动停止的实例 static Process hwMonitorProcess; static StreamWriter hwMonitorIn; @@ -633,9 +639,10 @@ static void RunHardwareMonitor() { } static void StartHardwareMonitor() { - if (hwMonitorProcess != null && !hwMonitorProcess.HasExited) return; - - hwMonitorProcess = new Process { + Process process; + lock (hwMonitorLifecycleLock) { + if (hwMonitorProcess != null && !hwMonitorProcess.HasExited) return; + process = new Process { StartInfo = new ProcessStartInfo { FileName = Application.ExecutablePath, Arguments = "--hwmonitor", @@ -646,9 +653,11 @@ static void StartHardwareMonitor() { CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden } - }; + }; + hwMonitorProcess = process; + } - hwMonitorProcess.OutputDataReceived += (s, e) => { + process.OutputDataReceived += (s, e) => { if (string.IsNullOrEmpty(e.Data)) return; //Debug.WriteLine("[HWMonitor OUT] " + e.Data); // 将子进程输出重定向到VS的输出窗口 if (e.Data.StartsWith("CRASH:")) return; @@ -699,35 +708,52 @@ static void StartHardwareMonitor() { } }; - hwMonitorProcess.ErrorDataReceived += (s, e) => { + process.ErrorDataReceived += (s, e) => { if (string.IsNullOrEmpty(e.Data)) return; Logger.Error("HardwareMonitor [HWMonitor ERR] " + e.Data); }; - hwMonitorProcess.EnableRaisingEvents = true; - hwMonitorProcess.Exited += (s, e) => { - if (hwMonitorStopping) { - hwMonitorStopping = false; - return; + process.EnableRaisingEvents = true; + process.Exited += (s, e) => { + bool intentional; + lock (hwMonitorLifecycleLock) { + intentional = hwMonitorStoppingProcesses.Remove(process); + if (ReferenceEquals(hwMonitorProcess, process)) { + hwMonitorProcess = null; + hwMonitorIn = null; + } } + if (intentional) return; //Logger.Info("StartHardwareMonitor [HWMonitor] 进程退出,准备重启..."); System.Threading.Tasks.Task.Delay(3000).ContinueWith(_ => { + if (!monitorCPU && !monitorGPU) return; try { StartHardwareMonitor(); } catch { } }); }; try { - hwMonitorProcess.Start(); - hwMonitorIn = hwMonitorProcess.StandardInput; - hwMonitorProcess.BeginOutputReadLine(); - hwMonitorProcess.BeginErrorReadLine(); // 必须读取错误流避免死锁 + process.Start(); + lock (hwMonitorLifecycleLock) { + if (ReferenceEquals(hwMonitorProcess, process)) hwMonitorIn = process.StandardInput; + } + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); // 必须读取错误流避免死锁 SetGpuMonitorState(monitorGPU); SetCpuMonitorState(monitorCPU); SetMonitorInterval(monitorRefreshRate == "high" ? 250 : 1000); - } catch (Exception) { } + } catch (Exception) { + lock (hwMonitorLifecycleLock) { + if (ReferenceEquals(hwMonitorProcess, process)) { + hwMonitorProcess = null; + hwMonitorIn = null; + } + } + try { process.Dispose(); } catch { } + } } static void SetGpuMonitorState(bool enable) { + if (enable && gpuSleepGuardActive) return; if (hwMonitorIn != null && hwMonitorProcess != null && !hwMonitorProcess.HasExited) { try { hwMonitorIn.WriteLine(enable ? "GPU:ON" : "GPU:OFF"); } catch { } } @@ -746,10 +772,54 @@ static void SetMonitorInterval(int ms) { } static void StopHardwareMonitor() { - if (hwMonitorProcess != null && !hwMonitorProcess.HasExited) { - hwMonitorStopping = true; - try { hwMonitorProcess.Kill(); } catch { hwMonitorStopping = false; } + StopHardwareMonitorAndWait(5000); + } + + static bool StopHardwareMonitorAndWait(int timeoutMs) { + Process process; + lock (hwMonitorLifecycleLock) { + process = hwMonitorProcess; + if (process == null) { + hwMonitorIn = null; + return true; + } + try { + if (process.HasExited) { + hwMonitorProcess = null; + hwMonitorIn = null; + return true; + } + } catch { + hwMonitorProcess = null; + hwMonitorIn = null; + return true; + } + hwMonitorStoppingProcesses.Add(process); + hwMonitorIn = null; } + + bool exited = false; + try { + process.Kill(); + exited = process.WaitForExit(timeoutMs); + if (!exited) return false; + } catch (InvalidOperationException) { + exited = true; + } catch { + return false; + } finally { + lock (hwMonitorLifecycleLock) { + if (exited) { + if (ReferenceEquals(hwMonitorProcess, process)) hwMonitorProcess = null; + } else { + hwMonitorStoppingProcesses.Remove(process); + if (ReferenceEquals(hwMonitorProcess, process)) { + try { hwMonitorIn = process.StandardInput; } catch { hwMonitorIn = null; } + } + } + } + } + return true; } static int flagStart = 0; @@ -1147,8 +1217,6 @@ static void SyncDataToTxt() { // 硬件传感器查询 private static int _isQuerying = 0; // 防重入标志,支持 Interlocked 原子操作 static int countQuery = 0; - static bool autoStartMonitorGPU = true, autoStopMonitorGPU = true;//是否自动根据情况开/关GPU温度监测以节约能源 - static bool hasStartAuto = false, hasStopAuto = false;//是否已经自动开/关过GPU温度监测,在手动开/关时重置 // 用于风扇查表的平滑温度(受高中低档影响) static float smoothedCPUTemp = 50f; static float smoothedGPUTemp = 40f; @@ -1186,6 +1254,8 @@ static void QueryHardware() { smoothedGPUTemp = rawTempGPU * respondSpeed + smoothedGPUTemp * (1.0f - respondSpeed); } + bool fanLockPowerHandledByProtection = false; + // 根据显示方式决定展示原始值或平滑值 if (monitorCPU && cpuTempReady) CPUTemp = (tempDisplayMode == "raw") ? tempCPU : smoothedCPUTemp; @@ -1203,26 +1273,47 @@ static void QueryHardware() { } if (fanSpeedCondition) { - // 先切换为降温模式(cool配置) - fanTable = "cool"; - LoadFanConfig("cool.txt"); - UpdateCheckedState("fanTableGroup", Strings.FanCoolMode); - SaveConfig("FanTable"); - - // 再切换为自动风扇控制 - fanControl = "auto"; - SetMaxFanSpeedOff(); - fanControlTimer.Change(0, 1000); - UpdateCheckedState("fanControlGroup", Strings.FanAuto); - SaveConfig("FanControl"); - - trayIcon.BalloonTipTitle = Strings.HighTempBalloonTitle; - trayIcon.BalloonTipText = Strings.HighTempBalloonText(currentMaxCPUTemp, smoothedCPUTemp); - trayIcon.BalloonTipIcon = ToolTipIcon.Warning; - trayIcon.ShowBalloonTip(3000); + bool exitedFanLockForSafety = false; + bool shouldSwitchFan = true; + if (currentPreset == FanLockPresetKey) { + shouldSwitchFan = PrepareFanLockOverheatProtection(); + if (shouldSwitchFan) { + LeaveFanLockPresetForSafety(); + exitedFanLockForSafety = true; + } else { + fanLockPowerHandledByProtection = true; + trayIcon.BalloonTipTitle = Strings.HighTempBalloonTitle; + trayIcon.BalloonTipText = Strings.FanLockPowerReducedBalloonText(currentMaxCPUTemp, smoothedCPUTemp, fanLockCurrentPower, fanLockMinimumPower); + trayIcon.BalloonTipIcon = ToolTipIcon.Warning; + trayIcon.ShowBalloonTip(3000); + } + } + + if (shouldSwitchFan) { + // 先切换为降温模式(cool配置) + fanTable = "cool"; + LoadFanConfig("cool.txt"); + UpdateCheckedState("fanTableGroup", Strings.FanCoolMode); + if (!exitedFanLockForSafety) SaveConfig("FanTable"); + + // 再切换为自动风扇控制 + fanControl = "auto"; + SetMaxFanSpeedOff(); + fanControlTimer.Change(0, 1000); + UpdateCheckedState("fanControlGroup", Strings.FanAuto); + if (!exitedFanLockForSafety) SaveConfig("FanControl"); + + trayIcon.BalloonTipTitle = Strings.HighTempBalloonTitle; + trayIcon.BalloonTipText = Strings.HighTempBalloonText(currentMaxCPUTemp, smoothedCPUTemp); + trayIcon.BalloonTipIcon = ToolTipIcon.Warning; + trayIcon.ShowBalloonTip(3000); + } } } + if (!fanLockPowerHandledByProtection) + AdjustFanLockPower(); + //通过countQuery延时来确保温度正常读取 if (countQuery <= 5 && monitorGPU) countQuery++; @@ -1428,6 +1519,7 @@ static string GetPresetDisplayName(string presetKey) { case "PresetExtreme": return Strings.PresetExtreme; case "PresetGpuPriority": return Strings.PresetGpuPriority; case "PresetLightUse": return Strings.PresetLightUse; + case FanLockPresetKey: return Strings.PresetFanLock; case "PresetCustom1": return presetCustom1Name; case "PresetCustom2": return presetCustom2Name; case "PresetCustom3": return presetCustom3Name; diff --git a/Strings.cs b/Strings.cs index cb2c8b1..0663fb9 100644 --- a/Strings.cs +++ b/Strings.cs @@ -59,6 +59,20 @@ public static class Strings { public static string PresetExtreme => T("极致性能", "極致性能", "Extreme Performance"); public static string PresetGpuPriority => T("GPU优先", "GPU優先", "GPU Priority"); public static string PresetLightUse => T("轻度使用", "輕度使用", "Light Use"); + public static string PresetFanLock => T("锁定风扇转速模式", "鎖定風扇轉速模式", "Locked Fan Speed Mode"); + public static string PresetFanLockTooltip => T( + "左键进入锁定风扇转速模式;右键修改目标转速、目标温度和CPU功耗范围。", + "左鍵進入鎖定風扇轉速模式;右鍵修改目標轉速、目標溫度和CPU功耗範圍。", + "Left-click to activate locked fan speed mode; right-click to edit its fan, temperature, and CPU power range targets."); + public static string FanLockEditorTitle => T("锁定风扇转速模式", "鎖定風扇轉速模式", "Locked Fan Speed Mode"); + public static string FanLockMinimumPower => T("CPU功耗墙下限 (W)", "CPU功耗牆下限 (W)", "Minimum CPU power (W)"); + public static string FanLockMaximumPower => T("CPU最大功率目标 (W)", "CPU最大功率目標 (W)", "Maximum CPU power target (W)"); + public static string FanLockTargetRpm => T("目标风扇转速 (RPM)", "目標風扇轉速 (RPM)", "Target fan speed (RPM)"); + public static string FanLockTargetTemperature => T("目标CPU温度 (℃)", "目標CPU溫度 (℃)", "Target CPU temperature (°C)"); + public static string FanLockNeedsCpuPower => T( + "当前预设没有设置CPU功率,无法确定锁定模式的PL1起点。请先设置一个CPU功率数值。", + "目前預設沒有設定CPU功率,無法確定鎖定模式的PL1起點。請先設定一個CPU功率數值。", + "The current preset has no CPU power setting, so the starting PL1 cannot be determined. Set a CPU power value first."); public static string PresetCustom1 => T("自定义预设1", "自定義預設1", "Custom 1"); public static string PresetCustom2 => T("自定义预设2", "自定義預設2", "Custom 2"); public static string PresetCustom3 => T("自定义预设3", "自定義預設3", "Custom 3"); @@ -209,6 +223,9 @@ public static string GfxSwitchedTo(string mode) => T( // ───────────────────────────────────────────────────────────────────────── public static string GpuAppsMenu => T("占用GPU的程序", "佔用GPU的程式", "GPU Processes"); public static string GpuAppsNone => T("无", "無", "None"); + public static string GpuAppsQueryMayWake => T("GPU 查询已暂停(可能唤醒独显)", + "GPU 查詢已暫停(可能喚醒獨顯)", "GPU query paused (may wake dGPU)"); + public static string GpuAppsQueryNow => T("立即查询一次", "立即查詢一次", "Query once now"); public static string GpuRestartMenu => T("重启显卡", "重啟顯示卡", "Restart GPU"); public static string GpuRestartTooltip => T("通过重启独立 GPU 减少不必要的占用 GPU 情况。", "透過重啟獨立 GPU 減少不必要的 GPU 佔用情況。", @@ -288,9 +305,13 @@ public static string DbUnlockFailed(float w) => T( // 高温警告(气泡) public static string HighTempBalloonTitle => T("温度过高警告", "溫度過高警告", "High Temperature Warning"); public static string HighTempBalloonText(int limit, float temp) => T( - $"检测到CPU温度高于{limit - 5}℃ ({temp:F1}℃),且风扇处于固定转速状态,OSH已自动切换为降温模式并将风扇控制切换为自动模式。", - $"偵測到CPU溫度高於{limit - 5}℃ ({temp:F1}℃),且風扇處於固定轉速狀態,OSH已自動切換至降溫模式並將風扇控制改為自動。", - $"CPU temperature exceeded {limit - 5}°C ({temp:F1}°C) with a fixed fan speed. OSH has switched to Cool mode and Auto fan control."); + $"检测到CPU温度高于{limit - 2}℃ ({temp:F1}℃),且风扇处于固定转速状态,OSH已自动切换为降温模式并将风扇控制切换为自动模式。", + $"偵測到CPU溫度高於{limit - 2}℃ ({temp:F1}℃),且風扇處於固定轉速狀態,OSH已自動切換至降溫模式並將風扇控制改為自動。", + $"CPU temperature exceeded {limit - 2}°C ({temp:F1}°C) with a fixed fan speed. OSH has switched to Cool mode and Auto fan control."); + public static string FanLockPowerReducedBalloonText(int limit, float temp, int power, int minimum) => T( + $"检测到CPU温度高于{limit - 2}℃ ({temp:F1}℃)。锁定风扇转速模式已优先将CPU功耗墙降低至{power} W,下限为{minimum} W;到达下限后若仍过热才会切换风扇控制。", + $"偵測到CPU溫度高於{limit - 2}℃ ({temp:F1}℃)。鎖定風扇轉速模式已優先將CPU功耗牆降低至{power} W,下限為{minimum} W;到達下限後若仍過熱才會切換風扇控制。", + $"CPU temperature exceeded {limit - 2}°C ({temp:F1}°C). Locked fan speed mode reduced CPU power to {power} W first (minimum {minimum} W); fan control will change only if overheating continues at the minimum."); // ───────────────────────────────────────────────────────────────────────── // 性能控制 — 提示文本 @@ -455,6 +476,7 @@ public static string SysNvidiaPowerLimitText(string limitsText) => T( public static string MonitorCpuOff => T("关闭CPU监控", "關閉CPU監控", "Disable CPU Monitor"); public static string MonitorGpuOn => T("开启GPU监控", "開啟GPU監控", "Enable GPU Monitor"); public static string MonitorGpuOff => T("关闭GPU监控", "關閉GPU監控", "Disable GPU Monitor"); + public static string MonitorGpuStopFailed => T("无法完全停止旧的硬件监控进程,GPU监控仍保持开启。", "無法完全停止舊的硬體監控程序,GPU 監控仍保持開啟。", "The previous hardware monitor could not be stopped completely. GPU monitoring remains enabled."); public static string MonitorFanOn => T("开启风扇监控", "開啟風扇監控", "Enable Fan Monitor"); public static string MonitorFanOff => T("关闭风扇监控", "關閉風扇監控", "Disable Fan Monitor"); public static string MonitorRefresh => T("刷新频率", "更新頻率", "Refresh Rate"); @@ -609,7 +631,7 @@ public static string OmenKeyShortcutSendFailed(int error) => T( public static string MonitorGpuPowerLabel => T("GPU功率", "GPU功率", "GPU Power"); public static string MonitorGpuFrequencyLabel => T("GPU频率", "GPU頻率", "GPU Frequency"); public static string MonitorFanLabel => T("风扇", "風扇", "Fan"); - public static string GpuPoweredOff => T("节能", "節能", "PoweredOff"); + public static string GpuPoweredOff => T("遥测不可用", "遙測無法使用", "Telemetry unavailable"); public static string MonitorPrepareLabel => T("数据获取中...", "數據獲取中...", "Retrieving data..."); // ───────────────────────────────────────────────────────────────────────── @@ -675,5 +697,46 @@ private static string T(string zh, string tw, string en) { public static string DeviceNotFound => T("未找到描述包含 NVIDIA 的显示适配器!", "未找到描述包含 NVIDIA 的顯示卡!", "Display adapter containing 'NVIDIA' not found!"); public static string RestartGPUSuccess => T("重启显卡成功!", "重啟顯示卡成功!", "Restart GPU successful!"); public static string RestartGPUFailed => T("重启显卡失败!", "重啟顯示卡失敗!", "Failed to restart GPU!"); + public static string Yes => T("是", "是", "Yes"); + public static string No => T("否", "否", "No"); + public static string GpuSleepMenu => T("强制尝试休眠独显", "強制嘗試休眠獨顯", "Force dGPU Sleep Attempt"); + public static string GpuSleepTooltip => T("清理独显占用、重启设备并尝试进入D3,失败时提供占用诊断和快捷修复。", "清理獨顯佔用、重新啟動裝置並嘗試進入 D3,失敗時提供診斷與快速修復。", "Clear dGPU clients, restart the device, and attempt D3 sleep with diagnostics and repair actions."); + public static string GpuSleepNeedsSecondGpu => T("未检测到可用的第二块物理显卡,禁止执行独显休眠。", "未偵測到可用的第二張實體顯示卡,禁止執行獨顯休眠。", "No usable second physical GPU was detected."); + public static string GpuSleepTargetNotFound => T("未找到可安全操作的NVIDIA独显。", "找不到可安全操作的 NVIDIA 獨顯。", "No eligible NVIDIA dGPU was found."); + public static string GpuSleepNoActiveFallbackGpu => T("第二块显卡没有活动显示模式,无法确认它能接管桌面。", "第二張顯示卡沒有作用中的顯示模式,無法確認可接管桌面。", "The fallback GPU has no active display mode."); + public static string GpuSleepTargetHasActiveDisplay => T("独显当前仍有活动显示模式,可能正在驱动内屏或外接显示器,已阻止休眠操作。", "獨顯目前仍有作用中的顯示模式,可能正在驅動內建或外接顯示器,已阻止休眠操作。", "The dGPU still has an active display mode and may be driving a display."); + public static string GpuSleepDiscreteModeBlocked => T("当前为独显直连模式,不能尝试休眠独显。", "目前為獨顯直連模式,不能嘗試休眠獨顯。", "dGPU sleep is unavailable in discrete graphics mode."); + public static string GpuSleepConfirm => T("该操作会停止GPU监控并重启独显,应用可能崩溃。是否继续?", "此操作會停止 GPU 監控並重新啟動獨顯,應用程式可能當機。是否繼續?", "This stops GPU monitoring and restarts the dGPU. Applications may crash. Continue?"); + public static string GpuSleepWorking => T("正在尝试让独显休眠,请稍候。", "正在嘗試讓獨顯休眠,請稍候。", "Attempting to put the dGPU to sleep."); + public static string GpuSleepSucceeded => T("独显已进入Windows报告的D3状态。GPU监控将保持关闭以免将其唤醒。", "獨顯已進入 Windows 回報的 D3 狀態。GPU 監控將保持關閉以避免喚醒。", "The dGPU entered the Windows-reported D3 state. GPU monitoring remains off."); + public static string GpuSleepMonitorStopFailed => T("无法完全停止硬件监控进程。为避免监控继续唤醒独显,本次休眠尝试已取消。", "無法完全停止硬體監控程序。為避免監控繼續喚醒獨顯,本次休眠嘗試已取消。", "The hardware monitor could not be stopped completely. The sleep attempt was cancelled to avoid waking the dGPU."); + public static string GpuSleepRestartFailed(string error) => T($"重启独显失败:{error}", $"重新啟動獨顯失敗:{error}", $"Failed to restart the dGPU: {error}"); + public static string GpuSleepFailedNoProcesses => T("独显仍未进入D3,但没有检测到可处理的普通应用。可能是显示链路、驱动服务或平台电源管理阻止了休眠。", "獨顯仍未進入 D3,但未偵測到可處理的一般應用程式。可能是顯示鏈路、驅動服務或平台電源管理所致。", "The dGPU did not enter D3 and no configurable application was found. Display routing, driver services, or platform power management may be responsible."); + public static string GpuSleepStillFailed => T("调整应用后独显仍未进入D3。请检查仍存在的进程、外接显示器、独显直连模式和其他硬件监控程序。", "調整應用程式後獨顯仍未進入 D3。請檢查剩餘程序、外接顯示器、獨顯直連模式與其他硬體監控程式。", "The dGPU still did not enter D3. Check remaining processes, external displays, discrete mode, and monitoring tools."); + public static string GpuSleepRepairTitle => T("独显休眠失败修复", "獨顯休眠失敗修復", "dGPU Sleep Repair"); + public static string GpuSleepRepairDescription => T("以下程序在休眠失败后仍被检测为使用独显。可将选中程序设为节能GPU,重启后再次尝试。所有修改均保存原始值并可回滚。", "以下程序在休眠失敗後仍被偵測為使用獨顯。可將選取程序設為節能 GPU,重新啟動後再次嘗試。所有修改皆可回復。", "These processes still use the dGPU. Set selected apps to the power-saving GPU, restart them, then retry. Changes are backed up and reversible."); + public static string GpuSleepProcessColumn => T("程序", "程序", "Process"); + public static string GpuSleepUsageColumn => T("GPU调用", "GPU 使用", "GPU use"); + public static string GpuSleepPreferenceColumn => T("当前偏好", "目前偏好", "Preference"); + public static string GpuSleepCustomListColumn => T("已在自定列表", "已在自訂清單", "Customized"); + public static string GpuSleepPathColumn => T("可执行文件路径", "執行檔路徑", "Executable path"); + public static string GpuPreferenceSystemDefault => T("系统决定", "系統決定", "System default"); + public static string GpuPreferencePowerSaving => T("节能GPU", "節能 GPU", "Power saving GPU"); + public static string GpuPreferenceHighPerformance => T("高性能GPU", "高效能 GPU", "High performance GPU"); + public static string GpuPreferenceUnavailable => T("不可用", "無法使用", "Unavailable"); + public static string GpuPreferenceApplyPowerSaving => T("设为节能GPU", "設為節能 GPU", "Use power-saving GPU"); + public static string GpuPreferenceRestore => T("恢复之前设置", "還原先前設定", "Restore previous settings"); + public static string GpuSleepRestartSelected => T("确认并重启应用", "確認並重新啟動應用程式", "Restart selected apps"); + public static string GpuSleepRetry => T("再次尝试休眠", "再次嘗試休眠", "Retry sleep"); + public static string GpuSleepSelectApplication => T("请至少选择一个可以设置GPU偏好的应用。", "請至少選擇一個可設定 GPU 偏好的應用程式。", "Select at least one configurable application."); + public static string GpuSleepDwmWarning => T("你选择了dwm.exe。重启桌面合成器时屏幕可能黑屏或闪烁,确认继续吗?", "你選擇了 dwm.exe。重新啟動桌面合成器時畫面可能黑屏或閃爍,確定繼續嗎?", "dwm.exe is selected. Restarting Desktop Window Manager may blank or flicker the display. Continue?"); + public static string GpuPreferenceAppliedRestartRequired => T("GPU偏好已写入。必须重启这些应用才能生效。", "GPU 偏好已寫入,必須重新啟動應用程式才會生效。", "GPU preferences were saved. Restart the applications to apply them."); + public static string GpuPreferenceApplyFailed(string error) => T($"设置GPU偏好失败:{error}", $"設定 GPU 偏好失敗:{error}", $"Failed to set GPU preference: {error}"); + public static string GpuSleepRestartWarning => T("将先尝试正常关闭,必要时强制结束选中的应用。未保存的数据可能丢失,确认继续吗?", "將先嘗試正常關閉,必要時強制結束選取的應用程式。未儲存資料可能遺失,確定繼續嗎?", "Selected apps will be closed and may be forcibly terminated. Unsaved data may be lost. Continue?"); + public static string GpuSleepApplicationsRestarted => T("应用已重启。现在可以再次尝试独显休眠。", "應用程式已重新啟動,現在可以再次嘗試獨顯休眠。", "Applications were restarted. You can retry dGPU sleep now."); + public static string GpuPreferenceBackupMissing => T("找不到GPU偏好设置备份。", "找不到 GPU 偏好設定備份。", "GPU preference backup was not found."); + public static string GpuPreferenceConflict => T("部分应用的设置在OSH修改后又发生变化。是否仍覆盖并恢复到最初状态?", "部分應用程式設定在 OSH 修改後又有變更。是否仍覆寫並還原最初狀態?", "Some preferences changed after OSH modified them. Overwrite those changes and restore the original values?"); + public static string GpuPreferenceRestoreFailed => T("恢复GPU偏好失败。", "還原 GPU 偏好失敗。", "Failed to restore GPU preferences."); + public static string GpuPreferenceRestoredRestartRequired => T("原始GPU偏好已恢复。相关应用需要再次重启。", "原始 GPU 偏好已還原,相關應用程式需要再次重新啟動。", "Original GPU preferences were restored. Restart the affected applications again."); } }