Initial commit: SpdUp Windows Gaming Booster v1.0.0
- .NET 8 WPF app with full MVVM architecture - One-click boost: CPU, RAM, GPU, network, services, timer resolution - Real-time system monitor (LibreHardwareMonitor) - Auto game detection (30+ games) - RAM optimizer with kernel-level cleanup - Network optimizer (TCP registry tweaks) - Service manager, process manager, shader cache cleaner - Desktop widget overlay - Debug log viewer - Custom dark gaming theme with trans pride colors - 84 unit tests (xUnit) - Inno Setup installer script - Landing page website
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
using System.Diagnostics;
|
||||
using SpdUp.Core;
|
||||
using SpdUp.Models;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Detects running games and known launchers.
|
||||
/// </summary>
|
||||
public sealed class GameDetectionService : IDisposable
|
||||
{
|
||||
private readonly Timer _pollTimer;
|
||||
private readonly AppSettings _settings;
|
||||
private readonly HashSet<string> _knownGameProcesses = new(StringComparer.OrdinalIgnoreCase);
|
||||
private string? _detectedGameProcess;
|
||||
|
||||
public event Action<string>? GameStarted;
|
||||
public event Action<string>? GameStopped;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known launcher processes that indicate a game may launch.
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> LauncherProcesses = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"steam", "steamwebhelper",
|
||||
"EpicGamesLauncher",
|
||||
"Battle.net", "Agent"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Heuristic: processes that look like games (high-memory GPU-using apps not in system dirs).
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> ExcludedProcessNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"explorer", "svchost", "System", "Idle", "csrss", "smss", "lsass",
|
||||
"services", "wininit", "winlogon", "dwm", "taskhostw", "RuntimeBroker",
|
||||
"ShellExperienceHost", "SearchHost", "SearchIndexer", "fontdrvhost",
|
||||
"conhost", "dllhost", "sihost", "ctfmon", "TextInputHost",
|
||||
"SecurityHealthSystray", "SecurityHealthService",
|
||||
"devenv", "Code", "chrome", "msedge", "firefox", "Discord",
|
||||
"Spotify", "slack", "Teams"
|
||||
};
|
||||
|
||||
public GameDetectionService(AppSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
|
||||
// Build known game process name set from settings.
|
||||
foreach (var g in _settings.Games)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(g.ProcessName))
|
||||
_knownGameProcesses.Add(g.ProcessName);
|
||||
}
|
||||
|
||||
_pollTimer = new Timer(PollForGames, null, Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public string? CurrentGameProcess => _detectedGameProcess;
|
||||
|
||||
public void Start(int intervalMs = 3000)
|
||||
{
|
||||
_pollTimer.Change(0, intervalMs);
|
||||
Logger.Info("Game detection started.");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_pollTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Logger.Info("Game detection stopped.");
|
||||
}
|
||||
|
||||
public void AddGameProcess(string processName)
|
||||
{
|
||||
_knownGameProcesses.Add(processName);
|
||||
}
|
||||
|
||||
private void PollForGames(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
var running = Process.GetProcesses();
|
||||
string? foundGame = null;
|
||||
|
||||
// First: check known game processes
|
||||
foreach (var proc in running)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_knownGameProcesses.Contains(proc.ProcessName))
|
||||
{
|
||||
foundGame = proc.ProcessName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { /* access denied */ }
|
||||
finally { proc.Dispose(); }
|
||||
}
|
||||
|
||||
// Second: heuristic – detect GPU-heavy, non-system, non-launcher processes
|
||||
if (foundGame is null)
|
||||
{
|
||||
foreach (var proc in Process.GetProcesses())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ExcludedProcessNames.Contains(proc.ProcessName)) continue;
|
||||
if (LauncherProcesses.Contains(proc.ProcessName)) continue;
|
||||
if (proc.Id <= 4) continue;
|
||||
|
||||
// Heuristic: >300 MB working set and has a main window => likely a game
|
||||
if (proc.WorkingSet64 > 300_000_000 && proc.MainWindowHandle != IntPtr.Zero)
|
||||
{
|
||||
// Check it's not from system folders
|
||||
try
|
||||
{
|
||||
var path = proc.MainModule?.FileName;
|
||||
if (path is not null &&
|
||||
!path.StartsWith(Environment.GetFolderPath(Environment.SpecialFolder.Windows), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foundGame = proc.ProcessName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { /* access denied */ }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { proc.Dispose(); }
|
||||
}
|
||||
}
|
||||
|
||||
// State machine: detect start / stop
|
||||
if (foundGame is not null && _detectedGameProcess is null)
|
||||
{
|
||||
_detectedGameProcess = foundGame;
|
||||
Logger.Info($"Game detected: {foundGame}");
|
||||
GameStarted?.Invoke(foundGame);
|
||||
}
|
||||
else if (foundGame is null && _detectedGameProcess is not null)
|
||||
{
|
||||
var prev = _detectedGameProcess;
|
||||
_detectedGameProcess = null;
|
||||
Logger.Info($"Game stopped: {prev}");
|
||||
GameStopped?.Invoke(prev);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Game detection poll error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pollTimer.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using LibreHardwareMonitor.Hardware;
|
||||
using SpdUp.Core;
|
||||
using SpdUp.Models;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Live system monitoring using performance counters + LibreHardwareMonitor.
|
||||
/// Designed for low overhead – one shared timer drives all consumers.
|
||||
/// </summary>
|
||||
public sealed class MonitoringService : IDisposable
|
||||
{
|
||||
private readonly Timer _timer;
|
||||
private readonly Computer _computer;
|
||||
private readonly PerformanceCounter _cpuCounter;
|
||||
private readonly NetworkService _networkService = new();
|
||||
private readonly string _pingTarget;
|
||||
|
||||
private SystemMetrics _latest = new();
|
||||
private bool _disposed;
|
||||
|
||||
public event Action<SystemMetrics>? MetricsUpdated;
|
||||
|
||||
public SystemMetrics Latest => _latest;
|
||||
|
||||
public MonitoringService(string pingTarget = "8.8.8.8")
|
||||
{
|
||||
_pingTarget = pingTarget;
|
||||
|
||||
// CPU % counter
|
||||
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total", readOnly: true);
|
||||
_cpuCounter.NextValue(); // prime the counter
|
||||
|
||||
// LibreHardwareMonitor for temps + GPU
|
||||
_computer = new Computer
|
||||
{
|
||||
IsCpuEnabled = true,
|
||||
IsGpuEnabled = true,
|
||||
IsMemoryEnabled = true,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_computer.Open();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"LibreHardwareMonitor init warning: {ex.Message}");
|
||||
}
|
||||
|
||||
_timer = new Timer(Tick, null, Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public void Start(int intervalMs = 1000)
|
||||
{
|
||||
_timer.Change(0, intervalMs);
|
||||
Logger.Info($"Monitoring started ({intervalMs} ms interval).");
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_timer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Logger.Info("Monitoring stopped.");
|
||||
}
|
||||
|
||||
public void SetInterval(int intervalMs)
|
||||
{
|
||||
_timer.Change(0, intervalMs);
|
||||
}
|
||||
|
||||
private async void Tick(object? state)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
float cpuUsage = _cpuCounter.NextValue();
|
||||
|
||||
// Memory
|
||||
var mem = new NativeMethods.MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<NativeMethods.MEMORYSTATUSEX>() };
|
||||
NativeMethods.GlobalMemoryStatusEx(ref mem);
|
||||
float ramTotal = mem.ullTotalPhys / 1048576f;
|
||||
float ramUsed = ramTotal - (mem.ullAvailPhys / 1048576f);
|
||||
|
||||
// Hardware sensors
|
||||
float cpuTemp = 0, gpuUsage = 0, gpuTemp = 0;
|
||||
try
|
||||
{
|
||||
foreach (var hw in _computer.Hardware)
|
||||
{
|
||||
hw.Update();
|
||||
foreach (var sensor in hw.Sensors)
|
||||
{
|
||||
if (hw.HardwareType == HardwareType.Cpu &&
|
||||
sensor.SensorType == SensorType.Temperature &&
|
||||
sensor.Name.Contains("Package", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
cpuTemp = sensor.Value ?? 0;
|
||||
}
|
||||
|
||||
if ((hw.HardwareType == HardwareType.GpuNvidia || hw.HardwareType == HardwareType.GpuAmd || hw.HardwareType == HardwareType.GpuIntel))
|
||||
{
|
||||
if (sensor.SensorType == SensorType.Load && sensor.Name.Contains("Core", StringComparison.OrdinalIgnoreCase))
|
||||
gpuUsage = sensor.Value ?? 0;
|
||||
if (sensor.SensorType == SensorType.Temperature && sensor.Name.Contains("Core", StringComparison.OrdinalIgnoreCase))
|
||||
gpuTemp = sensor.Value ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var sub in hw.SubHardware)
|
||||
{
|
||||
sub.Update();
|
||||
foreach (var sensor in sub.Sensors)
|
||||
{
|
||||
if (sensor.SensorType == SensorType.Temperature &&
|
||||
sensor.Name.Contains("Package", StringComparison.OrdinalIgnoreCase))
|
||||
cpuTemp = sensor.Value ?? cpuTemp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* sensor read failure – continue with zeros */ }
|
||||
|
||||
// Ping
|
||||
long pingMs = await NetworkService.PingAsync(_pingTarget, 1500);
|
||||
|
||||
_latest = new SystemMetrics
|
||||
{
|
||||
CpuUsage = cpuUsage,
|
||||
RamUsageMB = ramUsed,
|
||||
RamTotalMB = ramTotal,
|
||||
GpuUsage = gpuUsage,
|
||||
CpuTemperature = cpuTemp,
|
||||
GpuTemperature = gpuTemp,
|
||||
NetworkPingMs = pingMs,
|
||||
Fps = 0, // FPS hook not implemented without injection
|
||||
};
|
||||
|
||||
MetricsUpdated?.Invoke(_latest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Monitoring tick error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_timer.Dispose();
|
||||
_cpuCounter.Dispose();
|
||||
try { _computer.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Net.NetworkInformation;
|
||||
using Microsoft.Win32;
|
||||
using SpdUp.Core;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Network tweaks for gaming: DNS, TCP settings, latency checks.
|
||||
/// All changes are reversible via Restore methods.
|
||||
/// </summary>
|
||||
public sealed class NetworkService
|
||||
{
|
||||
private readonly Dictionary<string, object?> _previousRegValues = new();
|
||||
|
||||
/// <summary>
|
||||
/// Measure round-trip ping to a host.
|
||||
/// </summary>
|
||||
public static async Task<long> PingAsync(string host = "8.8.8.8", int timeoutMs = 2000)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ping = new Ping();
|
||||
var reply = await ping.SendPingAsync(host, timeoutMs);
|
||||
return reply.Status == IPStatus.Success ? reply.RoundtripTime : -1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply TCP gaming optimisations via the registry (Nagle, TCP ack frequency, etc.).
|
||||
/// </summary>
|
||||
public void ApplyTcpOptimizations()
|
||||
{
|
||||
try
|
||||
{
|
||||
const string tcpKey = @"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters";
|
||||
using var key = Registry.LocalMachine.OpenSubKey(tcpKey, writable: true);
|
||||
if (key is null) return;
|
||||
|
||||
SetRegistryValueWithBackup(key, tcpKey, "TcpAckFrequency", 1, RegistryValueKind.DWord);
|
||||
SetRegistryValueWithBackup(key, tcpKey, "TCPNoDelay", 1, RegistryValueKind.DWord);
|
||||
SetRegistryValueWithBackup(key, tcpKey, "TcpDelAckTicks", 0, RegistryValueKind.DWord);
|
||||
|
||||
// Also per-interface
|
||||
const string ifacesKey = @"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces";
|
||||
using var ifacesRoot = Registry.LocalMachine.OpenSubKey(ifacesKey);
|
||||
if (ifacesRoot is not null)
|
||||
{
|
||||
foreach (var subKeyName in ifacesRoot.GetSubKeyNames())
|
||||
{
|
||||
using var ifaceKey = ifacesRoot.OpenSubKey(subKeyName, writable: true);
|
||||
if (ifaceKey is null) continue;
|
||||
var fullPath = $"{ifacesKey}\\{subKeyName}";
|
||||
SetRegistryValueWithBackup(ifaceKey, fullPath, "TcpAckFrequency", 1, RegistryValueKind.DWord);
|
||||
SetRegistryValueWithBackup(ifaceKey, fullPath, "TCPNoDelay", 1, RegistryValueKind.DWord);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info("TCP gaming optimizations applied.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Failed to apply TCP optimizations", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore all TCP settings to their previous values.
|
||||
/// </summary>
|
||||
public void RestoreTcpSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var kvp in _previousRegValues)
|
||||
{
|
||||
var parts = kvp.Key.Split('|');
|
||||
if (parts.Length != 2) continue;
|
||||
|
||||
using var key = Registry.LocalMachine.OpenSubKey(parts[0], writable: true);
|
||||
if (key is null) continue;
|
||||
|
||||
if (kvp.Value is null)
|
||||
key.DeleteValue(parts[1], throwOnMissingValue: false);
|
||||
else
|
||||
key.SetValue(parts[1], kvp.Value);
|
||||
}
|
||||
_previousRegValues.Clear();
|
||||
Logger.Info("TCP settings restored.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Failed to restore TCP settings", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRegistryValueWithBackup(RegistryKey key, string keyPath, string valueName, object newValue, RegistryValueKind kind)
|
||||
{
|
||||
var backupKey = $"{keyPath}|{valueName}";
|
||||
if (!_previousRegValues.ContainsKey(backupKey))
|
||||
_previousRegValues[backupKey] = key.GetValue(valueName); // null if not present
|
||||
|
||||
key.SetValue(valueName, newValue, kind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using System.Diagnostics;
|
||||
using System.ServiceProcess;
|
||||
using SpdUp.Core;
|
||||
using SpdUp.Models;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the full boost lifecycle: activate → monitor → restore.
|
||||
/// </summary>
|
||||
public sealed class OptimizationService
|
||||
{
|
||||
private readonly RamOptimizationService _ramService = new();
|
||||
private readonly AppSettings _settings;
|
||||
|
||||
private readonly List<string> _stoppedServices = new();
|
||||
private uint _previousTimerResolution;
|
||||
private bool _timerResolutionChanged;
|
||||
private string? _boostedProcessName;
|
||||
private bool _isBoosted;
|
||||
|
||||
public bool IsBoosted => _isBoosted;
|
||||
public string? BoostedProcessName => _boostedProcessName;
|
||||
|
||||
public event Action<string>? BoostActivated;
|
||||
public event Action? BoostDeactivated;
|
||||
|
||||
public OptimizationService(AppSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate all boost optimizations. Thread-safe.
|
||||
/// </summary>
|
||||
public async Task<BoostResult> ActivateBoostAsync(string? gameProcessName = null)
|
||||
{
|
||||
if (_isBoosted) return new BoostResult(false, "Boost is already active.");
|
||||
|
||||
var messages = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Enable required privileges
|
||||
NativeMethods.EnablePrivilege(NativeMethods.SE_DEBUG_NAME);
|
||||
NativeMethods.EnablePrivilege(NativeMethods.SE_INC_BASE_PRIORITY_NAME);
|
||||
NativeMethods.EnablePrivilege(NativeMethods.SE_PROF_SINGLE_PROCESS_NAME);
|
||||
|
||||
// 2. RAM Cleanup
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var result = _ramService.FullCleanup();
|
||||
messages.Add($"RAM: trimmed {result.TrimmedCount} processes, standby purge {(result.StandbyPurged > 0 ? "OK" : "skipped")}.");
|
||||
});
|
||||
|
||||
// 3. Stop non-critical services
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var svcName in _settings.ServicesToStop)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sc = new ServiceController(svcName);
|
||||
if (sc.Status == ServiceControllerStatus.Running)
|
||||
{
|
||||
sc.Stop();
|
||||
_stoppedServices.Add(svcName);
|
||||
messages.Add($"Stopped service: {sc.DisplayName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Could not stop {svcName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Set game to HIGH priority
|
||||
if (!string.IsNullOrEmpty(gameProcessName))
|
||||
{
|
||||
_boostedProcessName = gameProcessName;
|
||||
await Task.Run(() => SetProcessPriority(gameProcessName, ProcessPriorityClass.High));
|
||||
messages.Add($"Set {gameProcessName} to HIGH priority.");
|
||||
}
|
||||
|
||||
// 5. Reduce timer resolution (0.5 ms)
|
||||
await Task.Run(() =>
|
||||
{
|
||||
NativeMethods.NtQueryTimerResolution(out _, out _, out _previousTimerResolution);
|
||||
int status = NativeMethods.NtSetTimerResolution(5000, true, out _);
|
||||
_timerResolutionChanged = status == 0;
|
||||
if (_timerResolutionChanged)
|
||||
messages.Add("Timer resolution set to 0.5 ms.");
|
||||
});
|
||||
|
||||
_isBoosted = true;
|
||||
BoostActivated?.Invoke(string.Join("\n", messages));
|
||||
Logger.Info("Boost activated: " + string.Join(" | ", messages));
|
||||
|
||||
return new BoostResult(true, string.Join("\n", messages));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Boost activation failed", ex);
|
||||
return new BoostResult(false, $"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore all changes made during boost.
|
||||
/// </summary>
|
||||
public async Task DeactivateBoostAsync()
|
||||
{
|
||||
if (!_isBoosted) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Restore timer resolution
|
||||
if (_timerResolutionChanged)
|
||||
{
|
||||
NativeMethods.NtSetTimerResolution(_previousTimerResolution, true, out _);
|
||||
_timerResolutionChanged = false;
|
||||
Logger.Info("Timer resolution restored.");
|
||||
}
|
||||
|
||||
// 2. Restart stopped services
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (var svcName in _stoppedServices)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sc = new ServiceController(svcName);
|
||||
if (sc.Status == ServiceControllerStatus.Stopped)
|
||||
{
|
||||
sc.Start();
|
||||
Logger.Info($"Restarted service: {sc.DisplayName}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Could not restart {svcName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
_stoppedServices.Clear();
|
||||
});
|
||||
|
||||
// 3. Restore process priority
|
||||
if (_boostedProcessName is not null)
|
||||
{
|
||||
await Task.Run(() => SetProcessPriority(_boostedProcessName, ProcessPriorityClass.Normal));
|
||||
_boostedProcessName = null;
|
||||
}
|
||||
|
||||
_isBoosted = false;
|
||||
BoostDeactivated?.Invoke();
|
||||
Logger.Info("Boost deactivated – all settings restored.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Boost deactivation error", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set priority class for a process by name.
|
||||
/// </summary>
|
||||
public static void SetProcessPriority(string processName, ProcessPriorityClass priority)
|
||||
{
|
||||
foreach (var proc in Process.GetProcessesByName(processName))
|
||||
{
|
||||
try
|
||||
{
|
||||
proc.PriorityClass = priority;
|
||||
Logger.Info($"Set {processName} (PID {proc.Id}) priority to {priority}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Cannot set priority for {processName}: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
proc.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill a process by PID.
|
||||
/// </summary>
|
||||
public static bool KillProcess(int pid)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var proc = Process.GetProcessById(pid);
|
||||
proc.Kill(entireProcessTree: true);
|
||||
Logger.Info($"Killed process {proc.ProcessName} (PID {pid}).");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error($"Failed to kill PID {pid}", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record BoostResult(bool Success, string Message);
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using SpdUp.Core;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Performs real memory optimizations: working-set trimming + standby-list purge.
|
||||
/// Requires administrator privileges.
|
||||
/// </summary>
|
||||
public sealed class RamOptimizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Trim working sets of all accessible processes – pushes unused pages to the standby list.
|
||||
/// </summary>
|
||||
public RamCleanupResult TrimWorkingSets()
|
||||
{
|
||||
int trimmed = 0, failed = 0;
|
||||
var ownPid = Environment.ProcessId;
|
||||
|
||||
foreach (var proc in Process.GetProcesses())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (proc.Id == ownPid || proc.Id == 0 || proc.Id == 4) continue;
|
||||
|
||||
var hProcess = NativeMethods.OpenProcess(
|
||||
NativeMethods.PROCESS_SET_INFORMATION | NativeMethods.PROCESS_QUERY_INFORMATION,
|
||||
false, proc.Id);
|
||||
|
||||
if (hProcess == IntPtr.Zero) { failed++; continue; }
|
||||
|
||||
try
|
||||
{
|
||||
if (NativeMethods.EmptyWorkingSet(hProcess))
|
||||
trimmed++;
|
||||
else
|
||||
failed++;
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.CloseHandle(hProcess);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed++;
|
||||
}
|
||||
finally
|
||||
{
|
||||
proc.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Info($"Working-set trim complete: {trimmed} trimmed, {failed} skipped.");
|
||||
return new RamCleanupResult(trimmed, failed, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Purge the standby memory list. Requires SeProfileSingleProcessPrivilege.
|
||||
/// </summary>
|
||||
public bool PurgeStandbyList()
|
||||
{
|
||||
try
|
||||
{
|
||||
NativeMethods.EnablePrivilege(NativeMethods.SE_PROF_SINGLE_PROCESS_NAME);
|
||||
|
||||
int command = (int)NativeMethods.SYSTEM_MEMORY_LIST_COMMAND.MemoryPurgeStandbyList;
|
||||
int status = NativeMethods.NtSetSystemInformation(
|
||||
NativeMethods.SystemMemoryListInformation,
|
||||
ref command,
|
||||
Marshal.SizeOf<int>());
|
||||
|
||||
if (status == 0)
|
||||
{
|
||||
Logger.Info("Standby list purged successfully.");
|
||||
return true;
|
||||
}
|
||||
|
||||
Logger.Warn($"NtSetSystemInformation returned NTSTATUS 0x{status:X8}");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Failed to purge standby list", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full RAM optimisation: trim working sets + purge standby.
|
||||
/// </summary>
|
||||
public RamCleanupResult FullCleanup()
|
||||
{
|
||||
var result = TrimWorkingSets();
|
||||
var purged = PurgeStandbyList();
|
||||
return result with { StandbyPurged = purged ? 1 : 0 };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns current memory statistics.
|
||||
/// </summary>
|
||||
public static (ulong totalMB, ulong availMB, uint loadPercent) GetMemoryStatus()
|
||||
{
|
||||
var mem = new NativeMethods.MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<NativeMethods.MEMORYSTATUSEX>() };
|
||||
NativeMethods.GlobalMemoryStatusEx(ref mem);
|
||||
return (mem.ullTotalPhys / 1048576, mem.ullAvailPhys / 1048576, mem.dwMemoryLoad);
|
||||
}
|
||||
}
|
||||
|
||||
public record struct RamCleanupResult(int TrimmedCount, int FailedCount, int StandbyPurged);
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Diagnostics;
|
||||
using System.ServiceProcess;
|
||||
using SpdUp.Core;
|
||||
using SpdUp.Models;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service management: list, stop, start Windows services.
|
||||
/// </summary>
|
||||
public static class ServiceManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// Non-critical services commonly stopped for gaming.
|
||||
/// </summary>
|
||||
public static readonly Dictionary<string, string> KnownNonCriticalServices = new()
|
||||
{
|
||||
["SysMain"] = "Superfetch / SysMain – pre-loads apps into RAM",
|
||||
["WSearch"] = "Windows Search indexing",
|
||||
["DiagTrack"] = "Connected User Experiences and Telemetry",
|
||||
["TabletInputService"] = "Touch Keyboard and Handwriting Panel",
|
||||
["MapsBroker"] = "Downloaded Maps Manager",
|
||||
["PcaSvc"] = "Program Compatibility Assistant",
|
||||
["wisvc"] = "Windows Insider Service",
|
||||
["WbioSrvc"] = "Windows Biometric Service",
|
||||
["Fax"] = "Fax service",
|
||||
["XblAuthManager"] = "Xbox Live Auth Manager",
|
||||
["XblGameSave"] = "Xbox Live Game Save",
|
||||
["XboxNetApiSvc"] = "Xbox Live Networking Service",
|
||||
};
|
||||
|
||||
public static List<ServiceEntry> GetServiceList(IEnumerable<string> serviceNames)
|
||||
{
|
||||
var entries = new List<ServiceEntry>();
|
||||
|
||||
foreach (var name in serviceNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sc = new ServiceController(name);
|
||||
entries.Add(new ServiceEntry
|
||||
{
|
||||
ServiceName = sc.ServiceName,
|
||||
DisplayName = sc.DisplayName,
|
||||
Status = sc.Status.ToString(),
|
||||
Description = KnownNonCriticalServices.GetValueOrDefault(name, "")
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
entries.Add(new ServiceEntry
|
||||
{
|
||||
ServiceName = name,
|
||||
DisplayName = name,
|
||||
Status = "NotFound"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public static bool StopService(string serviceName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sc = new ServiceController(serviceName);
|
||||
if (sc.Status == ServiceControllerStatus.Running)
|
||||
{
|
||||
sc.Stop();
|
||||
sc.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(15));
|
||||
Logger.Info($"Stopped service: {serviceName}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Logger.Error($"Failed to stop {serviceName}", ex); }
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool StartService(string serviceName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sc = new ServiceController(serviceName);
|
||||
if (sc.Status == ServiceControllerStatus.Stopped)
|
||||
{
|
||||
sc.Start();
|
||||
sc.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(15));
|
||||
Logger.Info($"Started service: {serviceName}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) { Logger.Error($"Failed to start {serviceName}", ex); }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.IO;
|
||||
using SpdUp.Core;
|
||||
|
||||
namespace SpdUp.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Cleans DirectX shader cache, NVIDIA shader cache, and temp files
|
||||
/// to reduce micro-stuttering in games.
|
||||
/// </summary>
|
||||
public static class ShaderCacheCleanerService
|
||||
{
|
||||
public static ShaderCleanResult CleanAll()
|
||||
{
|
||||
long totalBytes = 0;
|
||||
int totalFiles = 0;
|
||||
|
||||
totalBytes += CleanDirectory(GetDirectXShaderCachePath(), ref totalFiles);
|
||||
totalBytes += CleanDirectory(GetNvidiaShaderCachePath(), ref totalFiles);
|
||||
totalBytes += CleanDirectory(GetWindowsTempPath(), ref totalFiles);
|
||||
totalBytes += CleanDirectory(GetLocalTempPath(), ref totalFiles);
|
||||
|
||||
Logger.Info($"Shader cache clean: {totalFiles} files, {totalBytes / 1048576.0:F1} MB freed.");
|
||||
return new ShaderCleanResult(totalFiles, totalBytes);
|
||||
}
|
||||
|
||||
public static ShaderCleanResult CleanDirectXCache()
|
||||
{
|
||||
long totalBytes = 0;
|
||||
int totalFiles = 0;
|
||||
totalBytes += CleanDirectory(GetDirectXShaderCachePath(), ref totalFiles);
|
||||
return new ShaderCleanResult(totalFiles, totalBytes);
|
||||
}
|
||||
|
||||
public static ShaderCleanResult CleanNvidiaCache()
|
||||
{
|
||||
long totalBytes = 0;
|
||||
int totalFiles = 0;
|
||||
totalBytes += CleanDirectory(GetNvidiaShaderCachePath(), ref totalFiles);
|
||||
return new ShaderCleanResult(totalFiles, totalBytes);
|
||||
}
|
||||
|
||||
public static ShaderCleanResult CleanTempFiles()
|
||||
{
|
||||
long totalBytes = 0;
|
||||
int totalFiles = 0;
|
||||
totalBytes += CleanDirectory(GetWindowsTempPath(), ref totalFiles);
|
||||
totalBytes += CleanDirectory(GetLocalTempPath(), ref totalFiles);
|
||||
return new ShaderCleanResult(totalFiles, totalBytes);
|
||||
}
|
||||
|
||||
// ── Paths ──────────────────────────────────────────────────────────
|
||||
private static string GetDirectXShaderCachePath()
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"D3DSCache");
|
||||
|
||||
private static string GetNvidiaShaderCachePath()
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"NVIDIA", "DXCache");
|
||||
|
||||
private static string GetWindowsTempPath()
|
||||
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Temp");
|
||||
|
||||
private static string GetLocalTempPath()
|
||||
=> Path.GetTempPath();
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
private static long CleanDirectory(string path, ref int fileCount)
|
||||
{
|
||||
if (!Directory.Exists(path)) return 0;
|
||||
long bytes = 0;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
try
|
||||
{
|
||||
var fi = new FileInfo(file);
|
||||
bytes += fi.Length;
|
||||
fi.Delete();
|
||||
fileCount++;
|
||||
}
|
||||
catch { /* locked / in use – skip */ }
|
||||
}
|
||||
|
||||
// Remove empty dirs
|
||||
foreach (var dir in Directory.EnumerateDirectories(path, "*", SearchOption.AllDirectories)
|
||||
.OrderByDescending(d => d.Length))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.EnumerateFileSystemEntries(dir).Any())
|
||||
Directory.Delete(dir);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warn($"Error cleaning {path}: {ex.Message}");
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
public record struct ShaderCleanResult(int FilesDeleted, long BytesFreed)
|
||||
{
|
||||
public double MBFreed => BytesFreed / 1048576.0;
|
||||
}
|
||||
Reference in New Issue
Block a user