diff --git a/StandbyAndTimer/App.xaml.cs b/StandbyAndTimer/App.xaml.cs index ff31432..8ad0493 100644 --- a/StandbyAndTimer/App.xaml.cs +++ b/StandbyAndTimer/App.xaml.cs @@ -275,7 +275,7 @@ private static void DisablePowerThrottling() // user toggled it manually. bool ok = NativeMethods.SetProcessInformation( Process.GetCurrentProcess().Handle, - 4, + NativeMethods.ProcessPowerThrottling, ref state, System.Runtime.InteropServices.Marshal.SizeOf()); diff --git a/StandbyAndTimer/Services/Native/NativeMethods.cs b/StandbyAndTimer/Services/Native/NativeMethods.cs index 5b5c543..c5fa87a 100644 --- a/StandbyAndTimer/Services/Native/NativeMethods.cs +++ b/StandbyAndTimer/Services/Native/NativeMethods.cs @@ -40,8 +40,8 @@ internal static extern int NtSetSystemInformation( internal static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); /// - /// processInformationClass 34 = ProcessPowerThrottling. - /// Use PROCESS_POWER_THROTTLING_STATE (12 bytes) to disable EcoQoS / timer throttling. + /// Pass (4) for processInformationClass with + /// PROCESS_POWER_THROTTLING_STATE (12 bytes) to disable EcoQoS / timer throttling. /// [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool SetProcessInformation( @@ -54,16 +54,6 @@ internal static extern bool SetProcessInformation( [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] internal static extern uint SetThreadExecutionState(uint esFlags); - /// - /// Returns the system time updated **only** by the timer interrupt — its - /// delta between two distinct values equals the true active tick period. - /// Output is 100-ns FILETIME (8 bytes), so we marshal it as a long. - /// Do not confuse with GetSystemTimePreciseAsFileTime, which reads - /// QPC and would only give us call latency, not the timer rate. - /// - [DllImport("kernel32.dll")] - internal static extern void GetSystemTimeAsFileTime(out long lpSystemTimeAsFileTime); - /// Closes a kernel object handle (process token, file, etc.). [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] @@ -82,15 +72,6 @@ internal static extern bool SetProcessInformation( // Crucially excludes the legacy "current working directory" lookup. internal const uint LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = 0x00001000; - // ── winmm.dll ──────────────────────────────────────────────────────────── - - /// Multimedia timer: requests minimum 1 ms system timer period as a backup. - [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] - internal static extern uint TimeBeginPeriod(uint uPeriod); - - [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] - internal static extern uint TimeEndPeriod(uint uPeriod); - // ── avrt.dll ───────────────────────────────────────────────────────────── /// Registers this thread as a real-time multimedia task ("Pro Audio"). @@ -170,7 +151,7 @@ internal struct TOKEN_PRIVILEGES } /// - /// Passed to SetProcessInformation(processInformationClass=34). Version=1. + /// Passed to SetProcessInformation with processInformationClass = (4). Version=1. /// ControlMask = OR of bits this process wants to override (EXECUTION_SPEED, /// IGNORE_TIMER_RESOLUTION). For each controlled bit, StateMask=0 disables /// that throttle, bit set enables it. Bits absent from ControlMask follow @@ -190,6 +171,13 @@ internal struct PROCESS_POWER_THROTTLING_STATE internal const uint ES_CONTINUOUS = 0x80000000; internal const uint ES_SYSTEM_REQUIRED = 0x00000001; + // PROCESS_INFORMATION_CLASS enum value for ProcessPowerThrottling (winnt.h). + // Used as the processInformationClass arg to SetProcessInformation. A prior + // typo used 34 here — that value matches no class, so SetProcessInformation + // silently returned ERROR_INVALID_PARAMETER and the throttling state was + // never applied. Keep callers using this constant, never a literal. + internal const int ProcessPowerThrottling = 4; + // PROCESS_POWER_THROTTLING_STATE flags (winnt.h) internal const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1; // Win11+: when ControlMask has this bit AND StateMask=0, the OS honors diff --git a/StandbyAndTimer/Services/ProcessOptimizationService.cs b/StandbyAndTimer/Services/ProcessOptimizationService.cs index 3a59ab9..9906d4a 100644 --- a/StandbyAndTimer/Services/ProcessOptimizationService.cs +++ b/StandbyAndTimer/Services/ProcessOptimizationService.cs @@ -77,7 +77,7 @@ private static void ApplyTimerResolutionOptOut(Process p) { NativeMethods.SetProcessInformation( p.Handle, - 34, // ProcessPowerThrottling + NativeMethods.ProcessPowerThrottling, ref state, Marshal.SizeOf()); } diff --git a/StandbyAndTimer/Services/TimerResolutionService.cs b/StandbyAndTimer/Services/TimerResolutionService.cs index 38efb27..10f3f31 100644 --- a/StandbyAndTimer/Services/TimerResolutionService.cs +++ b/StandbyAndTimer/Services/TimerResolutionService.cs @@ -94,11 +94,7 @@ private uint ApplyTimerRequest() _ = NativeMethods.SetThreadExecutionState( NativeMethods.ES_CONTINUOUS | NativeMethods.ES_SYSTEM_REQUIRED); - // 4. AVRT multimedia scheduling for this thread. - uint taskIndex = 0; - NativeMethods.AvSetMmThreadCharacteristics("Pro Audio", ref taskIndex); - - // 5. Self-heal watchdog — dedicated Thread (NOT ThreadPool) that + // 4. Self-heal watchdog — dedicated Thread (NOT ThreadPool) that // re-asserts the request every 50 ms. The dedicated thread is the // key win over System.Threading.Timer: under heavy game load the // ThreadPool can briefly starve, delaying the callback by tens of @@ -121,42 +117,6 @@ private uint ApplyTimerRequest() return actualUnits; } - // Samples the system time and records the deltas between consecutive - // distinct values. Because GetSystemTimeAsFileTime is only updated on - // timer interrupt, the median delta is the true active timer period. - // - // Total wall time = samples * actualTick ≈ 60 * 0.5 ms = ~30 ms. Cheap. - private static double MeasureActualResolutionMs(int samples = 60) - { - try - { - var deltas = new long[samples]; - NativeMethods.GetSystemTimeAsFileTime(out long prev); - - int collected = 0; - while (collected < samples) - { - long curr; - do { NativeMethods.GetSystemTimeAsFileTime(out curr); } - while (curr == prev); - deltas[collected++] = curr - prev; - prev = curr; - } - - Array.Sort(deltas); - long medianUnits = deltas[samples / 2]; // 100-ns units - return medianUnits / 10_000.0; - } - catch (Exception ex) - { - // Fallback: report the requested resolution if sampling fails for - // any reason — better a slightly optimistic number than crashing - // the timer-activate path. - Logger.Warn($"MeasureActualResolutionMs failed: {ex.Message}"); - return NativeMethods.TARGET_TIMER_RESOLUTION / 10_000.0; - } - } - public void Deactivate() { if (!IsActive) return; diff --git a/StandbyAndTimer/StandbyAndTimer.csproj b/StandbyAndTimer/StandbyAndTimer.csproj index 09fc1ae..66973f7 100644 --- a/StandbyAndTimer/StandbyAndTimer.csproj +++ b/StandbyAndTimer/StandbyAndTimer.csproj @@ -24,7 +24,7 @@ this default, dev builds defaulted to AssemblyVersion 1.0.0.0 and the in-app update checker reported a phantom "new version" against the current release. --> - 2.0.4 + 2.0.5 diff --git a/StandbyAndTimer/ViewModels/MainViewModel.cs b/StandbyAndTimer/ViewModels/MainViewModel.cs index 83001e2..55170fa 100644 --- a/StandbyAndTimer/ViewModels/MainViewModel.cs +++ b/StandbyAndTimer/ViewModels/MainViewModel.cs @@ -79,8 +79,9 @@ public MainViewModel( _localization = localization; Settings = settingsViewModel; - Settings.SettingsChanged += (_, _) => PersistSettings(); - Settings.SnapshotProvider = BuildCurrentSettings; + Settings.SettingsChanged += (_, _) => PersistSettings(); + Settings.SettingsImported += (_, imported) => ApplySettings(imported); + Settings.SnapshotProvider = BuildCurrentSettings; _memoryService.SnapshotUpdated += OnSnapshotUpdated; _purgeService.PurgeSucceeded += OnPurgeSucceeded; @@ -129,16 +130,26 @@ private void ApplySettings(AppSettings s) { _isInitializing = false; } - SyncMonitorSettings(); + SyncMonitorThresholds(); + SyncMonitorGames(); } - private void SyncMonitorSettings() + // Pushes only the threshold / flag state to the monitor — does NOT rebuild + // the GamePaths list. Called on every TextBox keystroke, so the per-tick + // LINQ + List allocation that GamePaths rebuilding would cost is wasted. + private void SyncMonitorThresholds() { _memoryService.StandbyLimitMb = StandbyLimitMb; _memoryService.FreeLimitMb = FreeLimitMb; _memoryService.AutoPurgeEnabled = AutoPurgeEnabled; _memoryService.GameModeEnabled = GameModeEnabled; - _memoryService.GamePaths = Games.Select(g => g.ExecutablePath).ToList(); + } + + // Pushes the executable-path list to the monitor. Called only when the + // Games collection actually mutates (add / remove / settings load / import). + private void SyncMonitorGames() + { + _memoryService.GamePaths = Games.Select(g => g.ExecutablePath).ToList(); } private void PersistSettings() @@ -234,25 +245,25 @@ private void OnTimerResolutionMeasured(object? sender, double measuredMs) => partial void OnStandbyLimitMbChanged(int value) { - SyncMonitorSettings(); + SyncMonitorThresholds(); SchedulePersist(); // debounced — TextBox emits a change per keystroke } partial void OnFreeLimitMbChanged(int value) { - SyncMonitorSettings(); + SyncMonitorThresholds(); SchedulePersist(); // debounced — TextBox emits a change per keystroke } partial void OnAutoPurgeEnabledChanged(bool value) { - SyncMonitorSettings(); + SyncMonitorThresholds(); PersistSettings(); } partial void OnGameModeEnabledChanged(bool value) { - SyncMonitorSettings(); + SyncMonitorThresholds(); PersistSettings(); } @@ -322,7 +333,7 @@ public void AddGameFromPath(string path) DisplayName = Path.GetFileNameWithoutExtension(path), ExecutablePath = path }); - SyncMonitorSettings(); + SyncMonitorGames(); PersistSettings(); StatusMessage = _localization.GetString("Str_Status_GameAdded"); Logger.Info($"Game added: {path}"); @@ -334,7 +345,7 @@ private void RemoveGame() if (SelectedGame is null) return; Games.Remove(SelectedGame); SelectedGame = null; - SyncMonitorSettings(); + SyncMonitorGames(); PersistSettings(); StatusMessage = _localization.GetString("Str_Status_GameRemoved"); } diff --git a/StandbyAndTimer/ViewModels/SettingsViewModel.cs b/StandbyAndTimer/ViewModels/SettingsViewModel.cs index 1b967b4..a0fd3be 100644 --- a/StandbyAndTimer/ViewModels/SettingsViewModel.cs +++ b/StandbyAndTimer/ViewModels/SettingsViewModel.cs @@ -48,6 +48,11 @@ public sealed partial class SettingsViewModel : ObservableObject public event EventHandler? SettingsChanged; + // Raised after a successful ImportSettings — payload is the freshly loaded + // AppSettings. MainViewModel subscribes to re-apply values to the running + // session so the user doesn't have to restart for the import to take effect. + public event EventHandler? SettingsImported; + public Language[] LanguageOptions { get; } = [Language.English, Language.Turkish]; public Theme[] ThemeOptions { get; } = [Theme.Dark, Theme.Light, Theme.System]; @@ -167,6 +172,7 @@ private void ImportSettings() if (imported is null) throw new InvalidDataException("Empty settings file"); _settingsService.Save(imported); + SettingsImported?.Invoke(this, imported); UpdateStatus = _localization.GetString("Str_Settings_ImportOk"); Logger.Info($"Settings imported from {dlg.FileName}"); } diff --git a/StandbyAndTimer/Views/Strings/Strings.en-US.xaml b/StandbyAndTimer/Views/Strings/Strings.en-US.xaml index 6617cb5..79a5289 100644 --- a/StandbyAndTimer/Views/Strings/Strings.en-US.xaml +++ b/StandbyAndTimer/Views/Strings/Strings.en-US.xaml @@ -62,7 +62,7 @@ Launching installer… Download failed Settings exported - Settings imported — restart to apply + Settings imported Import failed Backup & Maintenance Open Log Folder diff --git a/StandbyAndTimer/Views/Strings/Strings.tr-TR.xaml b/StandbyAndTimer/Views/Strings/Strings.tr-TR.xaml index f55c6e8..1f6068d 100644 --- a/StandbyAndTimer/Views/Strings/Strings.tr-TR.xaml +++ b/StandbyAndTimer/Views/Strings/Strings.tr-TR.xaml @@ -62,7 +62,7 @@ Yükleyici başlatılıyor… İndirme başarısız Ayarlar dışa aktarıldı - Ayarlar yüklendi — uygulamayı yeniden başlatın + Ayarlar yüklendi İçe aktarma başarısız Yedekleme ve Bakım Log Klasörünü Aç diff --git a/build-installer.ps1 b/build-installer.ps1 index b2ffe7f..f789815 100644 --- a/build-installer.ps1 +++ b/build-installer.ps1 @@ -22,7 +22,7 @@ [CmdletBinding()] param( - [string]$Version = "2.0.4" + [string]$Version = "2.0.5" ) $ErrorActionPreference = "Stop" diff --git a/installer/Setup.iss b/installer/Setup.iss index 6499fd6..d8a5543 100644 --- a/installer/Setup.iss +++ b/installer/Setup.iss @@ -12,7 +12,7 @@ #define AppName "StandbyAndTimer" #ifndef AppVersion - #define AppVersion "2.0.4" + #define AppVersion "2.0.5" #endif #define AppPublisher "LAYE77IE" #define AppExeName "StandbyAndTimer.exe"