From 57acdfcf0045353e803ba1d3c96d4eae856535c6 Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:16:23 +0200 Subject: [PATCH 1/7] Refactor to show real-time CPU usage per process Replaces average CPU usage with real-time measurement in the process list. Pause process CPU usage updates, core usage updates and sorting updates when the window is hidden (in System-Tray). --- CPUSetSetter/Platforms/IProcessHandler.cs | 4 +- .../Windows/ProcessHandlerWindows.cs | 59 ++++++++----------- .../CoreUsage/CoreUsageControl.xaml.cs | 21 +++++-- .../Processes/ProcessListEntryViewModel.cs | 11 ++-- .../UI/Tabs/Processes/ProcessesTab.xaml | 8 +-- .../Tabs/Processes/ProcessesTabViewModel.cs | 32 ++++++++-- 6 files changed, 83 insertions(+), 52 deletions(-) diff --git a/CPUSetSetter/Platforms/IProcessHandler.cs b/CPUSetSetter/Platforms/IProcessHandler.cs index 3005e84..d85e831 100644 --- a/CPUSetSetter/Platforms/IProcessHandler.cs +++ b/CPUSetSetter/Platforms/IProcessHandler.cs @@ -6,10 +6,10 @@ namespace CPUSetSetter.Platforms public interface IProcessHandler : IDisposable { /// - /// Get the average CPU usage of the process of the recent past (~30 seconds) + /// Get the CPU usage of the process /// /// Between 0 and 1 on success. -1 on fail - double GetAverageCpuUsage(); + double GetCpuUsage(); bool ApplyMask(LogicalProcessorMask mask); } } diff --git a/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs b/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs index a4713fd..eb41cc5 100644 --- a/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs +++ b/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs @@ -11,7 +11,6 @@ namespace CPUSetSetter.Platforms.Windows public class ProcessHandlerWindows : IProcessHandler { private readonly static Dictionary _setIdPerLogicalProcessor; - private readonly Queue _cpuTimeMovingAverageBuffer = new(); private readonly string _executableName; private readonly uint _pid; @@ -32,46 +31,46 @@ public ProcessHandlerWindows(string executableName, uint pid, SafeProcessHandle _queryLimitedInfoHandle = queryHandle; } - public double GetAverageCpuUsage() + private bool _isFirstCall = true; + private long _lastTimeStamp = 0; + private TimeSpan _lastTotalCpuTime = TimeSpan.Zero; + + public double GetCpuUsage() { if (_queryLimitedInfoHandle.IsInvalid) { return -1; } - DateTime now = DateTime.Now; - // Remove datapoints older than 30 seconds from the moving average buffer - while (_cpuTimeMovingAverageBuffer.Count > 0) - { - TimeSpan datapointAge = now - _cpuTimeMovingAverageBuffer.Peek().Timestamp; - if (datapointAge.TotalSeconds > 30) - { - _cpuTimeMovingAverageBuffer.Dequeue(); - } - else - { - break; - } - } - - // Get the current total CPU time of the process + // Get the current CPU time using GetProcessTimes bool success = NativeMethods.GetProcessTimes(_queryLimitedInfoHandle, out FILETIME _, out FILETIME _, out FILETIME kernelTime, out FILETIME userTime); if (!success) { return -1; } + + long now = System.Diagnostics.Stopwatch.GetTimestamp(); TimeSpan totalCpuTime = TimeSpan.FromTicks((long)(kernelTime.ULong + userTime.ULong)); - _cpuTimeMovingAverageBuffer.Enqueue(new() { Timestamp = now, TotalCpuTime = totalCpuTime }); - // Take the CPU time from now and (up to) a minute ago, and get the average usage % - CpuTimeTimestamp startDatapoint = _cpuTimeMovingAverageBuffer.Peek(); - TimeSpan deltaTime = now - startDatapoint.Timestamp; - TimeSpan deltaCpuTime = totalCpuTime - startDatapoint.TotalCpuTime; + if (_isFirstCall) + { + _isFirstCall = false; + _lastTimeStamp = now; + _lastTotalCpuTime = totalCpuTime; + return 0; // There is no delta on the first call + } + + TimeSpan deltaTime = System.Diagnostics.Stopwatch.GetElapsedTime(_lastTimeStamp, now); + TimeSpan deltaCpuTime = totalCpuTime - _lastTotalCpuTime; + + _lastTimeStamp = now; + _lastTotalCpuTime = totalCpuTime; - if (deltaCpuTime.Ticks == 0) + if (deltaCpuTime.Ticks <= 0 || deltaTime.Ticks <= 0) return 0; - else - return (double)deltaCpuTime.Ticks / deltaTime.Ticks / CpuInfo.LogicalProcessorCount; + else if (CpuInfo.LogicalProcessorCount <= 0) + return 0; + return Math.Clamp((double)deltaCpuTime.Ticks / deltaTime.Ticks / CpuInfo.LogicalProcessorCount, 0, 1); } public bool ApplyMask(LogicalProcessorMask mask) @@ -276,7 +275,7 @@ private static Dictionary GetCpuSetIdPerLogicalProcessor() while (current < bufferEnd) { SYSTEM_CPU_SET_INFORMATION item = Marshal.PtrToStructure(current); - + if (item.Type != CPU_SET_INFORMATION_TYPE.CpuSetInformation) { throw new InvalidCastException("Invalid data type encountered; aborting"); @@ -299,13 +298,7 @@ public void Dispose() _queryLimitedInfoHandle.Dispose(); _setLimitedInfoHandle?.Dispose(); _setInfoHandle?.Dispose(); - _cpuTimeMovingAverageBuffer.Clear(); } - private class CpuTimeTimestamp - { - public DateTime Timestamp { get; init; } - public TimeSpan TotalCpuTime { get; init; } - } } } diff --git a/CPUSetSetter/UI/Tabs/Processes/CoreUsage/CoreUsageControl.xaml.cs b/CPUSetSetter/UI/Tabs/Processes/CoreUsage/CoreUsageControl.xaml.cs index a30714b..c225cd1 100644 --- a/CPUSetSetter/UI/Tabs/Processes/CoreUsage/CoreUsageControl.xaml.cs +++ b/CPUSetSetter/UI/Tabs/Processes/CoreUsage/CoreUsageControl.xaml.cs @@ -14,6 +14,9 @@ namespace CPUSetSetter.UI.Tabs.Processes.CoreUsage /// public partial class CoreUsageControl : UserControl { + private const int UPDATE_DELAY_VISIBLE_MS = 1000; + private const int UPDATE_DELAY_HIDDEN_MS = 2000; + private static bool _isRunning = false; private static List coreUsages = CpuInfo.LogicalProcessorNames.Select(cpuName => new CoreUsage(cpuName)).ToList(); @@ -140,6 +143,19 @@ private static async Task PerCoreUsageUpdateLoopInner(Dispatcher dispatcher) bool[] parkedValues = new bool[coreUsages.Count]; while (true) { + bool windowIsVisible = false; + await dispatcher.InvokeAsync(() => + { + windowIsVisible = App.Current.MainWindow.Visibility == Visibility.Visible; + }); + + if (!windowIsVisible) + { + // Skip the update if the window is not visible + await Task.Delay(UPDATE_DELAY_HIDDEN_MS); + continue; + } + for (int i = 0; i < coreUsages.Count; ++i) { // Get the Utility% of each logical processor, and clamp it between 0.0-1.0 @@ -152,11 +168,9 @@ private static async Task PerCoreUsageUpdateLoopInner(Dispatcher dispatcher) parkedValues[i] = false; } - bool windowIsVisible = false; // Apply the new values on the dispatcher to make sure changes are done in the same UI frame await dispatcher.InvokeAsync(() => { - windowIsVisible = App.Current.MainWindow.Visibility == Visibility.Visible; for (int i = 0; i < coreUsages.Count; ++i) { coreUsages[i].Utility = utilityValues[i]; @@ -164,8 +178,7 @@ await dispatcher.InvokeAsync(() => } }); - int delayTime = windowIsVisible ? 1500 : 6000; // Poll the CPU usage less often when not visible - await Task.Delay(delayTime); + await Task.Delay(UPDATE_DELAY_VISIBLE_MS); } } } diff --git a/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs b/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs index 87ac9a8..99cda79 100644 --- a/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs +++ b/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs @@ -19,8 +19,9 @@ public partial class ProcessListEntryViewModel : ObservableObject, IDisposable public string ImagePath { get; } [ObservableProperty] - [NotifyPropertyChangedFor(nameof(AverageCpuPercentageStr))] - private double _averageCpuUsage; + [NotifyPropertyChangedFor(nameof(CpuPercentageStr))] + private double _cpuUsage; + [ObservableProperty] private LogicalProcessorMask _mask; @@ -28,7 +29,7 @@ public partial class ProcessListEntryViewModel : ObservableObject, IDisposable [ObservableProperty] private bool _previousApplyFailed = false; - public string AverageCpuPercentageStr => AverageCpuUsage == -1 ? "" : $"{AverageCpuUsage * 100:F1}%"; + public string CpuPercentageStr => CpuUsage == -1 ? "" : $"{CpuUsage * 100:F1}%"; public ProcessListEntryViewModel(ProcessInfo pInfo) { @@ -44,12 +45,12 @@ public ProcessListEntryViewModel(ProcessInfo pInfo) SetMask(mask, false); _mask = mask; // _mask is already set by SetMask, this just suppresses a warning - AverageCpuUsage = _processHandler.GetAverageCpuUsage(); + CpuUsage = _processHandler.GetCpuUsage(); } public void UpdateCpuUsage() { - AverageCpuUsage = _processHandler.GetAverageCpuUsage(); + CpuUsage = _processHandler.GetCpuUsage(); } public bool SetMask(LogicalProcessorMask newMask, bool updateRule) diff --git a/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml b/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml index 1c7b22d..e722fac 100644 --- a/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml +++ b/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml @@ -73,10 +73,10 @@ Width="3*"> - - + ((ProcessListEntryViewModel)item).Name.Contains(ProcessNameFilter, StringComparison.OrdinalIgnoreCase); Task.Run(ProcessCpuUsageUpdateLoop); @@ -132,19 +135,40 @@ private void OnExitedProcess(uint exitedPid) private async Task ProcessCpuUsageUpdateLoop() { + bool wasVisible = true; + while (true) { bool windowIsVisible = false; await _dispatcher.InvokeAsync(() => { windowIsVisible = App.Current.MainWindow.Visibility == Visibility.Visible; + }); + + if (!windowIsVisible) + { + if (wasVisible) + { + await _dispatcher.InvokeAsync(PauseListUpdates); + wasVisible = false; + } + await Task.Delay(UPDATE_DELAY_HIDDEN_MS); + continue; + } + else if (!wasVisible) + { + await _dispatcher.InvokeAsync(ResumeListUpdates); + wasVisible = true; + } + + await _dispatcher.InvokeAsync(() => + { foreach (ProcessListEntryViewModel pEntry in RunningProcesses) { pEntry.UpdateCpuUsage(); } }); - int delayTime = windowIsVisible ? 1000 : 5000; // Poll the CPU usage less often when not visible - await Task.Delay(delayTime); + await Task.Delay(UPDATE_DELAY_VISIBLE_MS); } } } From 007f5017422aa4b358adcf4f3b32186caa36dcbb Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Fri, 3 Apr 2026 21:11:28 +0200 Subject: [PATCH 2/7] Update title bar color to match app theme --- CPUSetSetter/Themes/AppTheme.cs | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/CPUSetSetter/Themes/AppTheme.cs b/CPUSetSetter/Themes/AppTheme.cs index f90b913..58f77a0 100644 --- a/CPUSetSetter/Themes/AppTheme.cs +++ b/CPUSetSetter/Themes/AppTheme.cs @@ -1,5 +1,7 @@ using Microsoft.Win32; +using System.Runtime.InteropServices; using System.Windows; +using System.Windows.Interop; namespace CPUSetSetter.Themes @@ -9,6 +11,7 @@ public static class AppTheme public static void ApplyTheme(Theme theme) { string themePath; + bool isDark = false; switch (theme) { case Theme.Light: @@ -16,6 +19,7 @@ public static void ApplyTheme(Theme theme) break; case Theme.Dark: themePath = "Themes/DarkThemeColors.xaml"; + isDark = true; break; case Theme.System: ApplySystemTheme(); @@ -41,6 +45,8 @@ public static void ApplyTheme(Theme theme) { throw new InvalidOperationException($"Unexpected MergedDictionaries count: {mergedDicts.Count}"); } + + UpdateTitleBarStatus(isDark); } private static void ApplySystemTheme() @@ -54,6 +60,73 @@ private static void ApplySystemTheme() } ApplyTheme(Theme.Light); } + + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + + private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19; + private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; + + private static bool _isDarkTheme; + private static bool _isWindowLoadedRegistered; + + private static void UpdateTitleBarStatus(bool isDarkTheme) + { + if (Application.Current == null) return; + Application.Current.Dispatcher.VerifyAccess(); + + _isDarkTheme = isDarkTheme; + + if (!_isWindowLoadedRegistered) + { + EventManager.RegisterClassHandler(typeof(Window), FrameworkElement.LoadedEvent, new RoutedEventHandler(Window_Loaded)); + _isWindowLoadedRegistered = true; + } + + foreach (Window window in Application.Current.Windows) + { + var hwnd = new WindowInteropHelper(window).Handle; + if (hwnd == IntPtr.Zero) + { + window.SourceInitialized += OnSourceInitialized; + } + else + { + ApplyDarkMode(hwnd, _isDarkTheme); + } + } + } + + private static void OnSourceInitialized(object? sender, EventArgs e) + { + if (sender is Window window) + { + window.SourceInitialized -= OnSourceInitialized; + var hwnd = new WindowInteropHelper(window).Handle; + ApplyDarkMode(hwnd, _isDarkTheme); + } + } + + private static void Window_Loaded(object sender, RoutedEventArgs e) + { + if (sender is Window window) + { + var hwnd = new WindowInteropHelper(window).Handle; + if (hwnd != IntPtr.Zero) + { + ApplyDarkMode(hwnd, _isDarkTheme); + } + } + } + + private static void ApplyDarkMode(IntPtr hwnd, bool isDarkTheme) + { + int useImmersiveDarkMode = isDarkTheme ? 1 : 0; + if (DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useImmersiveDarkMode, sizeof(int)) != 0) + { + DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, ref useImmersiveDarkMode, sizeof(int)); + } + } } public enum Theme From edfa5426cccb1c7cd946226d31f898ded375e945 Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:40:43 +0200 Subject: [PATCH 3/7] Add in-app Windows Game Mode control - Add a CheckBox to the processes window for toggling Game Mode directly from the app - Allow disabling Game Mode through the GameModeWarning popup - Clear and reapply all active masks when Windows Game Mode is enabled/disabled - Add periodic polling to detect external Game Mode changes Allows quick switching for games that cause issues with CPU Set Setter without needing to close the app --- CPUSetSetter/App.xaml.cs | 1 + CPUSetSetter/Platforms/Windows/GameMode.cs | 69 +++++++++++++++ .../Platforms/Windows/GameModeWarning.cs | 57 +++++++------ .../Windows/ProcessHandlerWindows.cs | 7 +- CPUSetSetter/Themes/AppTheme.cs | 5 ++ CPUSetSetter/UI/MainWindow.xaml | 84 +++++++++++-------- CPUSetSetter/UI/MainWindow.xaml.cs | 24 +++++- .../Processes/ProcessListEntryViewModel.cs | 42 +++++++++- .../UI/Tabs/Processes/ProcessesTab.xaml | 5 +- .../Tabs/Processes/ProcessesTabViewModel.cs | 18 ++++ 10 files changed, 238 insertions(+), 74 deletions(-) create mode 100644 CPUSetSetter/Platforms/Windows/GameMode.cs diff --git a/CPUSetSetter/App.xaml.cs b/CPUSetSetter/App.xaml.cs index 6c35d7d..f87e916 100644 --- a/CPUSetSetter/App.xaml.cs +++ b/CPUSetSetter/App.xaml.cs @@ -1,5 +1,6 @@ using CPUSetSetter.Config.Models; using CPUSetSetter.Platforms; +using CPUSetSetter.Platforms.Windows; using CPUSetSetter.TrayIcon; using CPUSetSetter.UI; using CPUSetSetter.Util; diff --git a/CPUSetSetter/Platforms/Windows/GameMode.cs b/CPUSetSetter/Platforms/Windows/GameMode.cs new file mode 100644 index 0000000..ebe70d1 --- /dev/null +++ b/CPUSetSetter/Platforms/Windows/GameMode.cs @@ -0,0 +1,69 @@ +using CPUSetSetter.UI.Tabs.Processes; +using Microsoft.Win32; +using System; + +namespace CPUSetSetter.Platforms.Windows +{ + public static class GameMode + { + private const string GameBarKey = @"Software\Microsoft\GameBar"; + private const string ValueName = "AutoGameModeEnabled"; + + public static event EventHandler? IsEnabledChanged; + private static readonly object _stateLock = new(); + private static bool? _lastState; + + public static void CheckState() + { + bool raiseEvent = false; + + lock (_stateLock) + { + bool currentState = IsEnabled; + if (!_lastState.HasValue) + { + _lastState = currentState; + return; + } + if (_lastState != currentState) + { + _lastState = currentState; + raiseEvent = true; + } + } + + if (raiseEvent) + { + IsEnabledChanged?.Invoke(null, EventArgs.Empty); + } + } + + public static bool IsEnabled + { + get + { + using var key = Registry.CurrentUser.OpenSubKey(GameBarKey); + return key?.GetValue(ValueName, 0) is int intValue && intValue != 0; + } + set + { + try + { + using var key = Registry.CurrentUser.CreateSubKey(GameBarKey); + if (key == null) + { + WindowLogger.Write("Unable to toggle Windows Game Mode: Registry key could not be created or opened."); + return; + } + + key.SetValue(ValueName, value ? 1 : 0, RegistryValueKind.DWord); + CheckState(); + } + catch (Exception ex) + { + WindowLogger.Write($"Unable to toggle Windows Game Mode: {ex.Message}"); + } + } + } + } +} diff --git a/CPUSetSetter/Platforms/Windows/GameModeWarning.cs b/CPUSetSetter/Platforms/Windows/GameModeWarning.cs index 3bbcdc6..a5da9f0 100644 --- a/CPUSetSetter/Platforms/Windows/GameModeWarning.cs +++ b/CPUSetSetter/Platforms/Windows/GameModeWarning.cs @@ -1,11 +1,8 @@ using CPUSetSetter.Config.Models; using CPUSetSetter.UI.Tabs.Processes; -using Microsoft.Win32; -using System.Diagnostics; using System.Windows; - -namespace CPUSetSetter.Platforms +namespace CPUSetSetter.Platforms.Windows { /// /// Windows Game Mode is abysmal to gaming performance when combined with CPU Set Setter. @@ -13,23 +10,35 @@ namespace CPUSetSetter.Platforms /// public static class WindowsGameModeWarning { + private static bool _eventHooked = false; + private const string LogWarningMessage = "WARNING: Windows Game Mode is enabled. As long as Game Mode is enabled, no masks are applied."; + public static void ShowIfEnabled() { - if (!GameModeEnabled()) - return; + if (!_eventHooked) + { + _eventHooked = true; + GameMode.IsEnabledChanged += (s, e) => + { + if (GameMode.IsEnabled) + { + WindowLogger.Write(LogWarningMessage); + } + }; + } - WindowLogger.Write("WARNING: Windows Game Mode is enabled. This might have a severe negative impact on gaming performance/stability " + - "when also using CPU Set Setter on games."); + if (!GameMode.IsEnabled) + return; if (AppConfig.Instance.ShowGameModePopup) { - App.Current.Dispatcher.InvokeAsync(() => + Application.Current?.Dispatcher?.InvokeAsync(() => { MessageBoxResult result = MessageBox.Show( "Windows Game Mode is currently enabled.\n" + "On AMD CPUs, Game Mode is known to conflict with CPU Set Setter, leading to lower FPS and game crashes.\n" + "I am not sure if Intel CPUs are affected too. So if you have one, please share your findings with me on GitHub!\n\n" + - "Would you like to open the Game Mode Settings page?\n\n" + + "Would you like CPU Set Setter to disable Game Mode for you?\n\n" + "(This popup can be disabled in the Settings tab)", "CPU Set Setter: Windows Game Mode warning", MessageBoxButton.YesNo, @@ -37,28 +46,18 @@ public static void ShowIfEnabled() if (result == MessageBoxResult.Yes) { - OpenGameModeSettings(); + GameMode.IsEnabled = false; + } + else + { + WindowLogger.Write(LogWarningMessage); } }); } - } - - private static bool GameModeEnabled() - { - using RegistryKey? gameBarKey = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\GameBar"); - if (gameBarKey is null) - return false; - - object? gameModeValue = gameBarKey.GetValue("AutoGameModeEnabled"); - if (gameModeValue is int value) - return value != 0; - - return false; - } - - private static void OpenGameModeSettings() - { - Process.Start(new ProcessStartInfo { FileName = "ms-settings:gaming-gamemode", UseShellExecute = true }); + else + { + WindowLogger.Write(LogWarningMessage); + } } } } diff --git a/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs b/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs index eb41cc5..e0df4fa 100644 --- a/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs +++ b/CPUSetSetter/Platforms/Windows/ProcessHandlerWindows.cs @@ -2,6 +2,7 @@ using CPUSetSetter.UI.Tabs.Processes; using Microsoft.Win32.SafeHandles; using System.ComponentModel; +using System.Diagnostics; using System.Drawing; using System.Runtime.InteropServices; @@ -49,7 +50,7 @@ public double GetCpuUsage() return -1; } - long now = System.Diagnostics.Stopwatch.GetTimestamp(); + long now = Stopwatch.GetTimestamp(); TimeSpan totalCpuTime = TimeSpan.FromTicks((long)(kernelTime.ULong + userTime.ULong)); if (_isFirstCall) @@ -60,7 +61,7 @@ public double GetCpuUsage() return 0; // There is no delta on the first call } - TimeSpan deltaTime = System.Diagnostics.Stopwatch.GetElapsedTime(_lastTimeStamp, now); + TimeSpan deltaTime = Stopwatch.GetElapsedTime(_lastTimeStamp, now); TimeSpan deltaCpuTime = totalCpuTime - _lastTotalCpuTime; _lastTimeStamp = now; @@ -85,6 +86,8 @@ public bool ApplyMask(LogicalProcessorMask mask) result = ApplyCpuSet(mask); else if (_previousMaskType == MaskApplyType.Affinity) result = ApplyAffinity(mask); + else if (_previousMaskType == MaskApplyType.NoMask) + result = true; else throw new NotImplementedException(); break; diff --git a/CPUSetSetter/Themes/AppTheme.cs b/CPUSetSetter/Themes/AppTheme.cs index 58f77a0..12d25ec 100644 --- a/CPUSetSetter/Themes/AppTheme.cs +++ b/CPUSetSetter/Themes/AppTheme.cs @@ -61,6 +61,11 @@ private static void ApplySystemTheme() ApplyTheme(Theme.Light); } + /// + /// DwmSetWindowAttribute is used to set the title bar color on Windows 10 2004 and later. + /// On earlier versions, it will fail and falls back to the older attribute that only works on Windows 10 1903 and later. + /// + [DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); diff --git a/CPUSetSetter/UI/MainWindow.xaml b/CPUSetSetter/UI/MainWindow.xaml index 05cf8cc..054cf8e 100644 --- a/CPUSetSetter/UI/MainWindow.xaml +++ b/CPUSetSetter/UI/MainWindow.xaml @@ -14,41 +14,51 @@ Width="1020" MinWidth="900"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CPUSetSetter/UI/MainWindow.xaml.cs b/CPUSetSetter/UI/MainWindow.xaml.cs index c5030c5..34fe7ba 100644 --- a/CPUSetSetter/UI/MainWindow.xaml.cs +++ b/CPUSetSetter/UI/MainWindow.xaml.cs @@ -1,15 +1,18 @@ -using CPUSetSetter.UI.Tabs.Processes; +using CPUSetSetter.Platforms.Windows; +using CPUSetSetter.UI.Tabs.Processes; using System.ComponentModel; using System.Windows; using System.Windows.Input; - namespace CPUSetSetter.UI { public partial class MainWindow : Window { private bool _listIsPaused = false; + // Store the handler in a field for proper unsubscription + private readonly EventHandler _gameModeChangedHandler; + public MainWindow() { InitializeComponent(); @@ -19,6 +22,23 @@ public MainWindow() PreviewKeyUp += (_, e) => KeyReleased(e); Deactivated += (_, _) => ResumeListUpdates(); + + GameModeCheckBox.IsChecked = GameMode.IsEnabled; + GameModeCheckBox.Checked += (s, e) => GameMode.IsEnabled = true; + GameModeCheckBox.Unchecked += (s, e) => GameMode.IsEnabled = false; + + _gameModeChangedHandler = (s, e) => + { + Dispatcher.InvokeAsync(() => + { + if (GameModeCheckBox.IsChecked != GameMode.IsEnabled) + { + GameModeCheckBox.IsChecked = GameMode.IsEnabled; + } + }); + }; + GameMode.IsEnabledChanged += _gameModeChangedHandler; + Application.Current.Exit += (s, ev) => GameMode.IsEnabledChanged -= _gameModeChangedHandler; } protected override void OnClosing(CancelEventArgs e) diff --git a/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs b/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs index 99cda79..10d292a 100644 --- a/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs +++ b/CPUSetSetter/UI/Tabs/Processes/ProcessListEntryViewModel.cs @@ -2,6 +2,7 @@ using CPUSetSetter.Config.Models; using CPUSetSetter.Util; using CPUSetSetter.Platforms; +using CPUSetSetter.Platforms.Windows; namespace CPUSetSetter.UI.Tabs.Processes @@ -24,8 +25,23 @@ public partial class ProcessListEntryViewModel : ObservableObject, IDisposable [ObservableProperty] + [NotifyPropertyChangedFor(nameof(DisplayedMask))] private LogicalProcessorMask _mask; + public LogicalProcessorMask DisplayedMask + { + get => GameMode.IsEnabled ? LogicalProcessorMask.NoMask : (Mask ?? LogicalProcessorMask.NoMask); + set + { + if (!GameMode.IsEnabled && value != null) + { + Mask = value; + } + } + } + + public bool IsMaskAllowed => !GameMode.IsEnabled; + [ObservableProperty] private bool _previousApplyFailed = false; @@ -73,11 +89,22 @@ public bool SetMask(LogicalProcessorMask newMask, bool updateRule) } ruleSuccess = programRule.SetMask(newMask, true); } - bool success = _processHandler.ApplyMask(newMask); + bool success = ApplyMaskConsideringGameMode(newMask); PreviousApplyFailed = !success; return success && ruleSuccess; } + public void RefreshGameModeApply() + { + ApplyMaskConsideringGameMode(Mask); + } + + public void UpdateVisualMask() + { + OnPropertyChanged(nameof(DisplayedMask)); + OnPropertyChanged(nameof(IsMaskAllowed)); + } + /// /// The UI picked a different mask /// @@ -92,7 +119,18 @@ partial void OnMaskChanged(LogicalProcessorMask? oldValue, LogicalProcessorMask private void OnMaskEdited(object? sender, EventArgs e) { // One of the logical processors in the mask or the type has changed, apply it - PreviousApplyFailed = !_processHandler.ApplyMask(Mask); + bool success = ApplyMaskConsideringGameMode(Mask); + PreviousApplyFailed = !success; + } + + private bool ApplyMaskConsideringGameMode(LogicalProcessorMask mask) + { + if (GameMode.IsEnabled) + { + return _processHandler.ApplyMask(LogicalProcessorMask.NoMask); + } + + return _processHandler.ApplyMask(mask); } /// diff --git a/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml b/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml index e722fac..7fde97a 100644 --- a/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml +++ b/CPUSetSetter/UI/Tabs/Processes/ProcessesTab.xaml @@ -85,12 +85,13 @@ + SelectedValue="{Binding DisplayedMask, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" + IsEnabled="{Binding IsMaskAllowed}"> diff --git a/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs b/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs index f9d239f..f7754e9 100644 --- a/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs +++ b/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CPUSetSetter.Config.Models; using CPUSetSetter.Platforms; +using CPUSetSetter.Platforms.Windows; using CPUSetSetter.Util; using System.ComponentModel; using System.Windows; @@ -40,6 +41,15 @@ public ProcessesTabViewModel(Dispatcher dispatcher) runningProcessesView.LiveSortingProperties.Add(nameof(ProcessListEntryViewModel.CpuUsage)); runningProcessesView.Filter = item => ((ProcessListEntryViewModel)item).Name.Contains(ProcessNameFilter, StringComparison.OrdinalIgnoreCase); + GameMode.IsEnabledChanged += (s, e) => + { + foreach (var process in RunningProcesses) + { + process.RefreshGameModeApply(); + process.UpdateVisualMask(); + } + }; + Task.Run(ProcessCpuUsageUpdateLoop); } @@ -48,6 +58,11 @@ public ProcessesTabViewModel(Dispatcher dispatcher) /// public void OnMaskHotkeyPressed(LogicalProcessorMask mask) { + if (GameMode.IsEnabled) + { + return; + } + ProcessListEntryViewModel? foregroundProcess = GetCurrentForegroundProcess(); if (foregroundProcess is not null) { @@ -139,6 +154,9 @@ private async Task ProcessCpuUsageUpdateLoop() while (true) { + // Periodically sync Windows Game Mode registry changes + GameMode.CheckState(); + bool windowIsVisible = false; await _dispatcher.InvokeAsync(() => { From a7541bd2cd4b39a47e0bbf9d9d1164cae7c33335 Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Sun, 5 Apr 2026 02:31:28 +0200 Subject: [PATCH 4/7] Add Game Mode toggle to system tray menu --- CPUSetSetter.TrayIcon/AppTrayIcon.cs | 25 +++++++++++++++++++++++++ CPUSetSetter/App.xaml.cs | 8 ++++++++ 2 files changed, 33 insertions(+) diff --git a/CPUSetSetter.TrayIcon/AppTrayIcon.cs b/CPUSetSetter.TrayIcon/AppTrayIcon.cs index 41ccb2d..ccc5d77 100644 --- a/CPUSetSetter.TrayIcon/AppTrayIcon.cs +++ b/CPUSetSetter.TrayIcon/AppTrayIcon.cs @@ -6,14 +6,39 @@ namespace CPUSetSetter.TrayIcon public class AppTrayIcon : IDisposable { private readonly NotifyIcon? _trayIcon; + private readonly ToolStripMenuItem _gameModeItem; + private bool _isGameModeEnabled; public event EventHandler? OpenClicked; public event EventHandler? CloseClicked; + public event EventHandler? GameModeToggled; + + public bool IsGameModeEnabled + { + get => _isGameModeEnabled; + set + { + _isGameModeEnabled = value; + _gameModeItem.Text = value ? "Disable Game Mode" : "Enable Game Mode"; + } + } public AppTrayIcon(Stream iconStream) { ContextMenuStrip trayMenu = new(); trayMenu.Items.Add("Open", null, (_, _) => OpenClicked?.Invoke(this, new())); + trayMenu.Items.Add(new ToolStripSeparator()); + + _gameModeItem = new ToolStripMenuItem(); + IsGameModeEnabled = false; // This triggers the setter and sets the initial text + _gameModeItem.Click += (_, _) => + { + IsGameModeEnabled = !_isGameModeEnabled; + GameModeToggled?.Invoke(this, _isGameModeEnabled); + }; + trayMenu.Items.Add(_gameModeItem); + + trayMenu.Items.Add(new ToolStripSeparator()); trayMenu.Items.Add("Close", null, (_, _) => CloseClicked?.Invoke(this, new())); _trayIcon = new() { diff --git a/CPUSetSetter/App.xaml.cs b/CPUSetSetter/App.xaml.cs index f87e916..481afbf 100644 --- a/CPUSetSetter/App.xaml.cs +++ b/CPUSetSetter/App.xaml.cs @@ -78,6 +78,14 @@ protected override void OnStartup(StartupEventArgs e) trayIcon.OpenClicked += (_, _) => ShowMainWindow(); trayIcon.CloseClicked += (_, _) => ExitAppGracefully(); + // Sync the Game Mode state between the tray icon and the GameMode class + trayIcon.IsGameModeEnabled = GameMode.IsEnabled; + trayIcon.GameModeToggled += (_, isEnabled) => GameMode.IsEnabled = isEnabled; + GameMode.IsEnabledChanged += (_, _) => + { + Dispatcher.InvokeAsync(() => trayIcon.IsGameModeEnabled = GameMode.IsEnabled); + }; + if (AppConfig.Instance.IsFirstRun) { // Promote the tray icon directly onto the Taskbar instead of in the "up-arrow" menu From 2ac03d7f9cdc130f8e7a14bc1d830773b3681691 Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Sun, 5 Apr 2026 18:32:11 +0200 Subject: [PATCH 5/7] Improve WindowLogger to collapse repeated log messages Reduces log clutter when processes have the same name, like Discord or browsers --- CPUSetSetter/UI/Tabs/Processes/WindowLogger.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/CPUSetSetter/UI/Tabs/Processes/WindowLogger.cs b/CPUSetSetter/UI/Tabs/Processes/WindowLogger.cs index c818dd3..6c4fd90 100644 --- a/CPUSetSetter/UI/Tabs/Processes/WindowLogger.cs +++ b/CPUSetSetter/UI/Tabs/Processes/WindowLogger.cs @@ -8,7 +8,7 @@ public partial class WindowLogger : ObservableObject [ObservableProperty] private string _text = ""; - private readonly Queue _logLines = new(); + private readonly List<(string Message, int Count)> _logLines = new(); private readonly Lock _lock = new(); private bool _isUpdating = false; @@ -23,7 +23,15 @@ private void WriteImp(string message) { using (_lock.EnterScope()) { - _logLines.Enqueue(message + "\n"); + if (_logLines.Count > 0 && _logLines[^1].Message == message) + { + var last = _logLines[^1]; + _logLines[^1] = (last.Message, last.Count + 1); + } + else + { + _logLines.Add((message, 1)); + } // Begin updating the logger text in the UI // A small delay is used before updating, so multiple logs can be rendered in one go @@ -43,9 +51,9 @@ private async Task UpdateText() { while (_logLines.Count > 50) { - _logLines.Dequeue(); + _logLines.RemoveAt(0); } - Text = string.Join("", _logLines); + Text = string.Join("", _logLines.Select(l => l.Count > 1 ? $"{l.Message} ({l.Count})\n" : $"{l.Message}\n")); _isUpdating = false; } } From d8d20a9d2c8662677f449d643286c4f85dc6909d Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:08:46 +0200 Subject: [PATCH 6/7] Clarified logic for setting immersive dark mode for title bar --- CPUSetSetter/Themes/AppTheme.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CPUSetSetter/Themes/AppTheme.cs b/CPUSetSetter/Themes/AppTheme.cs index 12d25ec..dff306f 100644 --- a/CPUSetSetter/Themes/AppTheme.cs +++ b/CPUSetSetter/Themes/AppTheme.cs @@ -61,15 +61,12 @@ private static void ApplySystemTheme() ApplyTheme(Theme.Light); } - /// - /// DwmSetWindowAttribute is used to set the title bar color on Windows 10 2004 and later. - /// On earlier versions, it will fail and falls back to the older attribute that only works on Windows 10 1903 and later. - /// - [DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize); + // On Windows 10 20H1 (build 19041) and later, attribute 20 is the correct value. private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19; + // Fallback for Windows 10 1903/1909 (builds 18362–18363), where attribute 19 was used instead. private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20; private static bool _isDarkTheme; @@ -127,8 +124,12 @@ private static void Window_Loaded(object sender, RoutedEventArgs e) private static void ApplyDarkMode(IntPtr hwnd, bool isDarkTheme) { int useImmersiveDarkMode = isDarkTheme ? 1 : 0; + + // Try the modern attribute first (Windows 10 20H1+). if (DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useImmersiveDarkMode, sizeof(int)) != 0) { + // If it failed, fall back to the pre-20H1 attribute (Windows 10 1903/1909). + // On versions older than 1903, this will also fail silently with no effect. DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, ref useImmersiveDarkMode, sizeof(int)); } } From 0a08b3c033f69b2faf0730a92c9d6f71185dd54a Mon Sep 17 00:00:00 2001 From: RegentScar <188844243+RegentScar@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:45:09 +0200 Subject: [PATCH 7/7] Add option to disable mask clearing when Game Mode is enabled --- CPUSetSetter/Config/AppConfigFile.cs | 4 ++++ CPUSetSetter/Config/Models/AppConfig.cs | 5 +++++ .../Platforms/Windows/GameModeWarning.cs | 10 +++++---- CPUSetSetter/UI/MainWindow.xaml | 1 + .../Processes/ProcessListEntryViewModel.cs | 10 +++++---- .../Tabs/Processes/ProcessesTabViewModel.cs | 22 +++++++++++++++++-- .../UI/Tabs/Settings/SettingsTab.xaml | 9 ++++++++ 7 files changed, 51 insertions(+), 10 deletions(-) diff --git a/CPUSetSetter/Config/AppConfigFile.cs b/CPUSetSetter/Config/AppConfigFile.cs index 0cb1906..44bf84f 100644 --- a/CPUSetSetter/Config/AppConfigFile.cs +++ b/CPUSetSetter/Config/AppConfigFile.cs @@ -178,6 +178,7 @@ private static AppConfig JsonToConfig(ConfigJson configJson, bool generateDefaul configJson.ShowGameModePopup, configJson.ShowUpdatePopup, configJson.ClearMasksOnClose, + configJson.DisableGameModeMaskClearing, Enum.Parse(configJson.UiTheme), generateDefaultMasks, isFirstRun, @@ -196,6 +197,7 @@ private class ConfigJson public bool ShowGameModePopup { get; init; } public bool ShowUpdatePopup { get; init; } public bool ClearMasksOnClose { get; init; } + public bool DisableGameModeMaskClearing { get; init; } public string UiTheme { get; init; } public int ConfigVersion { get; init; } // Can be used in the future to migrate config files @@ -215,6 +217,7 @@ private ConfigJson() ShowGameModePopup = true; ShowUpdatePopup = true; ClearMasksOnClose = false; + DisableGameModeMaskClearing = false; UiTheme = Theme.System.ToString(); ConfigVersion = 0; } @@ -255,6 +258,7 @@ public ConfigJson(AppConfig config) ShowGameModePopup = config.ShowGameModePopup; ShowUpdatePopup = config.ShowUpdatePopup; ClearMasksOnClose = config.ClearMasksOnClose; + DisableGameModeMaskClearing = config.DisableGameModeMaskClearing; UiTheme = config.UiTheme.ToString(); ConfigVersion = AppConfigFile.ConfigVersion; } diff --git a/CPUSetSetter/Config/Models/AppConfig.cs b/CPUSetSetter/Config/Models/AppConfig.cs index 70f9e81..e93c99c 100644 --- a/CPUSetSetter/Config/Models/AppConfig.cs +++ b/CPUSetSetter/Config/Models/AppConfig.cs @@ -40,6 +40,9 @@ public partial class AppConfig : ObservableConfigObject [ObservableProperty] private bool _clearMasksOnClose; + [ObservableProperty] + private bool _disableGameModeMaskClearing; + [ObservableProperty] private Theme _uiTheme; @@ -57,6 +60,7 @@ public AppConfig(List logicalProcessorMasks, bool showGameModePopup, bool showUpdatePopup, bool clearMasksOnClose, + bool disableGameModeMaskClearing, Theme uiTheme, bool generateDefaultMasks, bool isFirstRun, @@ -77,6 +81,7 @@ public AppConfig(List logicalProcessorMasks, _showGameModePopup = showGameModePopup; _showUpdatePopup = showUpdatePopup; _clearMasksOnClose = clearMasksOnClose; + _disableGameModeMaskClearing = disableGameModeMaskClearing; _uiTheme = uiTheme; IsFirstRun = isFirstRun; diff --git a/CPUSetSetter/Platforms/Windows/GameModeWarning.cs b/CPUSetSetter/Platforms/Windows/GameModeWarning.cs index a5da9f0..802a073 100644 --- a/CPUSetSetter/Platforms/Windows/GameModeWarning.cs +++ b/CPUSetSetter/Platforms/Windows/GameModeWarning.cs @@ -11,7 +11,7 @@ namespace CPUSetSetter.Platforms.Windows public static class WindowsGameModeWarning { private static bool _eventHooked = false; - private const string LogWarningMessage = "WARNING: Windows Game Mode is enabled. As long as Game Mode is enabled, no masks are applied."; + private const string LogWarningMessage = "WARNING: While Windows Game Mode is enabled, no masks are applied. (This behavior can be disabled in the Settings tab)"; public static void ShowIfEnabled() { @@ -20,7 +20,7 @@ public static void ShowIfEnabled() _eventHooked = true; GameMode.IsEnabledChanged += (s, e) => { - if (GameMode.IsEnabled) + if (GameMode.IsEnabled && !AppConfig.Instance.DisableGameModeMaskClearing) { WindowLogger.Write(LogWarningMessage); } @@ -50,13 +50,15 @@ public static void ShowIfEnabled() } else { - WindowLogger.Write(LogWarningMessage); + if (!AppConfig.Instance.DisableGameModeMaskClearing) + WindowLogger.Write(LogWarningMessage); } }); } else { - WindowLogger.Write(LogWarningMessage); + if (!AppConfig.Instance.DisableGameModeMaskClearing) + WindowLogger.Write(LogWarningMessage); } } } diff --git a/CPUSetSetter/UI/MainWindow.xaml b/CPUSetSetter/UI/MainWindow.xaml index 054cf8e..97f1c88 100644 --- a/CPUSetSetter/UI/MainWindow.xaml +++ b/CPUSetSetter/UI/MainWindow.xaml @@ -15,6 +15,7 @@ MinWidth="900"> + GameMode.IsEnabled && !AppConfig.Instance.DisableGameModeMaskClearing; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(CpuPercentageStr))] private double _cpuUsage; @@ -30,17 +32,17 @@ public partial class ProcessListEntryViewModel : ObservableObject, IDisposable public LogicalProcessorMask DisplayedMask { - get => GameMode.IsEnabled ? LogicalProcessorMask.NoMask : (Mask ?? LogicalProcessorMask.NoMask); + get => GameModeForcesNoMask ? LogicalProcessorMask.NoMask : (Mask ?? LogicalProcessorMask.NoMask); set { - if (!GameMode.IsEnabled && value != null) + if (!GameModeForcesNoMask && value != null) { Mask = value; } } } - public bool IsMaskAllowed => !GameMode.IsEnabled; + public bool IsMaskAllowed => !GameModeForcesNoMask; [ObservableProperty] private bool _previousApplyFailed = false; @@ -125,7 +127,7 @@ private void OnMaskEdited(object? sender, EventArgs e) private bool ApplyMaskConsideringGameMode(LogicalProcessorMask mask) { - if (GameMode.IsEnabled) + if (GameModeForcesNoMask) { return _processHandler.ApplyMask(LogicalProcessorMask.NoMask); } diff --git a/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs b/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs index f7754e9..0f4e0e9 100644 --- a/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs +++ b/CPUSetSetter/UI/Tabs/Processes/ProcessesTabViewModel.cs @@ -41,11 +41,29 @@ public ProcessesTabViewModel(Dispatcher dispatcher) runningProcessesView.LiveSortingProperties.Add(nameof(ProcessListEntryViewModel.CpuUsage)); runningProcessesView.Filter = item => ((ProcessListEntryViewModel)item).Name.Contains(ProcessNameFilter, StringComparison.OrdinalIgnoreCase); + AppConfig.Instance.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(AppConfig.DisableGameModeMaskClearing) && GameMode.IsEnabled) + { + foreach (var process in RunningProcesses) + { + process.RefreshGameModeApply(); + // UpdateVisualMask is called to ensure UI bindings (like IsMaskAllowed and DisplayedMask) immediately update when the user changes this setting. + process.UpdateVisualMask(); + } + } + }; + GameMode.IsEnabledChanged += (s, e) => { + bool shouldRefresh = !AppConfig.Instance.DisableGameModeMaskClearing; foreach (var process in RunningProcesses) { - process.RefreshGameModeApply(); + if (shouldRefresh) + process.RefreshGameModeApply(); + + // UpdateVisualMask must always be called when GameMode toggles to ensure the UI correctly updates the combobox logic, + // regardless of whether we actively re-apply the masks to the underlying process. process.UpdateVisualMask(); } }; @@ -58,7 +76,7 @@ public ProcessesTabViewModel(Dispatcher dispatcher) /// public void OnMaskHotkeyPressed(LogicalProcessorMask mask) { - if (GameMode.IsEnabled) + if (GameMode.IsEnabled && !AppConfig.Instance.DisableGameModeMaskClearing) { return; } diff --git a/CPUSetSetter/UI/Tabs/Settings/SettingsTab.xaml b/CPUSetSetter/UI/Tabs/Settings/SettingsTab.xaml index 2701f43..c05be6a 100644 --- a/CPUSetSetter/UI/Tabs/Settings/SettingsTab.xaml +++ b/CPUSetSetter/UI/Tabs/Settings/SettingsTab.xaml @@ -61,6 +61,15 @@ + + + + + + + +