Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion StandbyAndTimer/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NativeMethods.PROCESS_POWER_THROTTLING_STATE>());

Expand Down
32 changes: 10 additions & 22 deletions StandbyAndTimer/Services/Native/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ internal static extern int NtSetSystemInformation(
internal static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);

/// <summary>
/// processInformationClass 34 = ProcessPowerThrottling.
/// Use PROCESS_POWER_THROTTLING_STATE (12 bytes) to disable EcoQoS / timer throttling.
/// Pass <see cref="ProcessPowerThrottling"/> (4) for processInformationClass with
/// PROCESS_POWER_THROTTLING_STATE (12 bytes) to disable EcoQoS / timer throttling.
/// </summary>
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool SetProcessInformation(
Expand All @@ -54,16 +54,6 @@ internal static extern bool SetProcessInformation(
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern uint SetThreadExecutionState(uint esFlags);

/// <summary>
/// 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 <c>GetSystemTimePreciseAsFileTime</c>, which reads
/// QPC and would only give us call latency, not the timer rate.
/// </summary>
[DllImport("kernel32.dll")]
internal static extern void GetSystemTimeAsFileTime(out long lpSystemTimeAsFileTime);

/// <summary>Closes a kernel object handle (process token, file, etc.).</summary>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
Expand All @@ -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 ────────────────────────────────────────────────────────────

/// <summary>Multimedia timer: requests minimum 1 ms system timer period as a backup.</summary>
[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 ─────────────────────────────────────────────────────────────

/// <summary>Registers this thread as a real-time multimedia task ("Pro Audio").</summary>
Expand Down Expand Up @@ -170,7 +151,7 @@ internal struct TOKEN_PRIVILEGES
}

/// <summary>
/// Passed to SetProcessInformation(processInformationClass=34). Version=1.
/// Passed to SetProcessInformation with processInformationClass = <see cref="ProcessPowerThrottling"/> (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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion StandbyAndTimer/Services/ProcessOptimizationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private static void ApplyTimerResolutionOptOut(Process p)
{
NativeMethods.SetProcessInformation(
p.Handle,
34, // ProcessPowerThrottling
NativeMethods.ProcessPowerThrottling,
ref state,
Marshal.SizeOf<NativeMethods.PROCESS_POWER_THROTTLING_STATE>());
}
Expand Down
42 changes: 1 addition & 41 deletions StandbyAndTimer/Services/TimerResolutionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion StandbyAndTimer/StandbyAndTimer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<Version>2.0.4</Version>
<Version>2.0.5</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
33 changes: 22 additions & 11 deletions StandbyAndTimer/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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}");
Expand All @@ -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");
}
Expand Down
6 changes: 6 additions & 0 deletions StandbyAndTimer/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppSettings>? SettingsImported;

public Language[] LanguageOptions { get; } = [Language.English, Language.Turkish];
public Theme[] ThemeOptions { get; } = [Theme.Dark, Theme.Light, Theme.System];

Expand Down Expand Up @@ -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}");
}
Expand Down
2 changes: 1 addition & 1 deletion StandbyAndTimer/Views/Strings/Strings.en-US.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<sys:String x:Key="Str_Settings_LaunchingInstaller">Launching installer…</sys:String>
<sys:String x:Key="Str_Settings_DownloadFailed">Download failed</sys:String>
<sys:String x:Key="Str_Settings_ExportOk">Settings exported</sys:String>
<sys:String x:Key="Str_Settings_ImportOk">Settings imported — restart to apply</sys:String>
<sys:String x:Key="Str_Settings_ImportOk">Settings imported</sys:String>
<sys:String x:Key="Str_Settings_ImportFail">Import failed</sys:String>
<sys:String x:Key="Str_Settings_BackupSection">Backup &amp; Maintenance</sys:String>
<sys:String x:Key="Str_Settings_OpenLog">Open Log Folder</sys:String>
Expand Down
2 changes: 1 addition & 1 deletion StandbyAndTimer/Views/Strings/Strings.tr-TR.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<sys:String x:Key="Str_Settings_LaunchingInstaller">Yükleyici başlatılıyor…</sys:String>
<sys:String x:Key="Str_Settings_DownloadFailed">İndirme başarısız</sys:String>
<sys:String x:Key="Str_Settings_ExportOk">Ayarlar dışa aktarıldı</sys:String>
<sys:String x:Key="Str_Settings_ImportOk">Ayarlar yüklendi — uygulamayı yeniden başlatın</sys:String>
<sys:String x:Key="Str_Settings_ImportOk">Ayarlar yüklendi</sys:String>
<sys:String x:Key="Str_Settings_ImportFail">İçe aktarma başarısız</sys:String>
<sys:String x:Key="Str_Settings_BackupSection">Yedekleme ve Bakım</sys:String>
<sys:String x:Key="Str_Settings_OpenLog">Log Klasörünü Aç</sys:String>
Expand Down
2 changes: 1 addition & 1 deletion build-installer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

[CmdletBinding()]
param(
[string]$Version = "2.0.4"
[string]$Version = "2.0.5"
)

$ErrorActionPreference = "Stop"
Expand Down
2 changes: 1 addition & 1 deletion installer/Setup.iss
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading