From 19ba425e79b0c6638e375fd790b047a2ebe297ba Mon Sep 17 00:00:00 2001 From: Phil-icyou <70935362+Phil-icyou@users.noreply.github.com> Date: Sun, 8 Mar 2026 17:34:45 +0100 Subject: [PATCH] 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 --- .gitignore | 28 + App.xaml | 13 + App.xaml.cs | 49 ++ AssemblyInfo.cs | 10 + Converters/Converters.cs | 86 +++ Core/Logger.cs | 71 ++ Core/NativeMethods.cs | 153 ++++ Core/ObservableObject.cs | 23 + Core/RelayCommand.cs | 56 ++ Installer/SpdUpSetup.iss | 60 ++ LICENSE | 21 + MainWindow.xaml | 254 +++++++ MainWindow.xaml.cs | 81 +++ Models/AppSettings.cs | 77 +++ Models/GameEntry.cs | 33 + Models/ProcessEntry.cs | 15 + Models/ServiceEntry.cs | 13 + Models/SystemMetrics.cs | 18 + README.md | 130 ++++ Services/GameDetectionService.cs | 157 +++++ Services/MonitoringService.cs | 157 +++++ Services/NetworkService.cs | 107 +++ Services/OptimizationService.cs | 208 ++++++ Services/RamOptimizationService.cs | 111 +++ Services/ServiceManagementService.cs | 96 +++ Services/ShaderCacheCleanerService.cs | 110 +++ SpdUp.Tests/ConverterTests.cs | 175 +++++ SpdUp.Tests/GlobalUsings.cs | 1 + SpdUp.Tests/LoggerTests.cs | 117 ++++ SpdUp.Tests/ModelTests.cs | 224 ++++++ SpdUp.Tests/ObservableObjectTests.cs | 93 +++ SpdUp.Tests/RelayCommandTests.cs | 99 +++ SpdUp.Tests/ServiceTests.cs | 150 ++++ SpdUp.Tests/SpdUp.Tests.csproj | 30 + SpdUp.Tests/ViewModelTests.cs | 157 +++++ SpdUp.csproj | 25 + SpdUp.sln | 51 ++ Themes/DarkTheme.xaml | 254 +++++++ ViewModels/BoostViewModel.cs | 46 ++ ViewModels/DashboardViewModel.cs | 16 + ViewModels/DebugLogViewModel.cs | 74 ++ ViewModels/GameLibraryViewModel.cs | 72 ++ ViewModels/MainViewModel.cs | 264 +++++++ ViewModels/MonitorViewModel.cs | 16 + ViewModels/ProcessManagerViewModel.cs | 100 +++ ViewModels/ServiceManagerViewModel.cs | 67 ++ ViewModels/SettingsViewModel.cs | 91 +++ Views/BoostView.xaml | 55 ++ Views/BoostView.xaml.cs | 8 + Views/DashboardView.xaml | 147 ++++ Views/DashboardView.xaml.cs | 8 + Views/DebugLogView.xaml | 69 ++ Views/DebugLogView.xaml.cs | 38 + Views/GameLibraryView.xaml | 26 + Views/GameLibraryView.xaml.cs | 8 + Views/MonitorView.xaml | 114 +++ Views/MonitorView.xaml.cs | 8 + Views/ProcessManagerView.xaml | 31 + Views/ProcessManagerView.xaml.cs | 8 + Views/ServiceManagerView.xaml | 28 + Views/ServiceManagerView.xaml.cs | 8 + Views/SettingsView.xaml | 83 +++ Views/SettingsView.xaml.cs | 8 + Views/WidgetWindow.xaml | 122 ++++ Views/WidgetWindow.xaml.cs | 83 +++ app.manifest | 19 + build-installer.ps1 | 86 +++ website/index.html | 960 ++++++++++++++++++++++++++ 68 files changed, 6176 insertions(+) create mode 100644 .gitignore create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 AssemblyInfo.cs create mode 100644 Converters/Converters.cs create mode 100644 Core/Logger.cs create mode 100644 Core/NativeMethods.cs create mode 100644 Core/ObservableObject.cs create mode 100644 Core/RelayCommand.cs create mode 100644 Installer/SpdUpSetup.iss create mode 100644 LICENSE create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cs create mode 100644 Models/AppSettings.cs create mode 100644 Models/GameEntry.cs create mode 100644 Models/ProcessEntry.cs create mode 100644 Models/ServiceEntry.cs create mode 100644 Models/SystemMetrics.cs create mode 100644 README.md create mode 100644 Services/GameDetectionService.cs create mode 100644 Services/MonitoringService.cs create mode 100644 Services/NetworkService.cs create mode 100644 Services/OptimizationService.cs create mode 100644 Services/RamOptimizationService.cs create mode 100644 Services/ServiceManagementService.cs create mode 100644 Services/ShaderCacheCleanerService.cs create mode 100644 SpdUp.Tests/ConverterTests.cs create mode 100644 SpdUp.Tests/GlobalUsings.cs create mode 100644 SpdUp.Tests/LoggerTests.cs create mode 100644 SpdUp.Tests/ModelTests.cs create mode 100644 SpdUp.Tests/ObservableObjectTests.cs create mode 100644 SpdUp.Tests/RelayCommandTests.cs create mode 100644 SpdUp.Tests/ServiceTests.cs create mode 100644 SpdUp.Tests/SpdUp.Tests.csproj create mode 100644 SpdUp.Tests/ViewModelTests.cs create mode 100644 SpdUp.csproj create mode 100644 SpdUp.sln create mode 100644 Themes/DarkTheme.xaml create mode 100644 ViewModels/BoostViewModel.cs create mode 100644 ViewModels/DashboardViewModel.cs create mode 100644 ViewModels/DebugLogViewModel.cs create mode 100644 ViewModels/GameLibraryViewModel.cs create mode 100644 ViewModels/MainViewModel.cs create mode 100644 ViewModels/MonitorViewModel.cs create mode 100644 ViewModels/ProcessManagerViewModel.cs create mode 100644 ViewModels/ServiceManagerViewModel.cs create mode 100644 ViewModels/SettingsViewModel.cs create mode 100644 Views/BoostView.xaml create mode 100644 Views/BoostView.xaml.cs create mode 100644 Views/DashboardView.xaml create mode 100644 Views/DashboardView.xaml.cs create mode 100644 Views/DebugLogView.xaml create mode 100644 Views/DebugLogView.xaml.cs create mode 100644 Views/GameLibraryView.xaml create mode 100644 Views/GameLibraryView.xaml.cs create mode 100644 Views/MonitorView.xaml create mode 100644 Views/MonitorView.xaml.cs create mode 100644 Views/ProcessManagerView.xaml create mode 100644 Views/ProcessManagerView.xaml.cs create mode 100644 Views/ServiceManagerView.xaml create mode 100644 Views/ServiceManagerView.xaml.cs create mode 100644 Views/SettingsView.xaml create mode 100644 Views/SettingsView.xaml.cs create mode 100644 Views/WidgetWindow.xaml create mode 100644 Views/WidgetWindow.xaml.cs create mode 100644 app.manifest create mode 100644 build-installer.ps1 create mode 100644 website/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a7046cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Compiled output +bin/ +obj/ +publish/ + +# IDE +.vs/ +*.user +*.suo + +# Temp files +*_wpftmp.csproj +*.rsuser + +# NuGet +*.nupkg +packages/ + +# Build +*.dll +*.exe +*.pdb + +# Test results +TestResults/ + +# Inno Setup output +Installer/Output/ diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..2f78a3e --- /dev/null +++ b/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..145ee86 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,49 @@ +using System.Windows; +using System.Windows.Threading; +using SpdUp.Core; + +namespace SpdUp; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + // Global exception handlers + DispatcherUnhandledException += OnDispatcherUnhandledException; + AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + + // Enable required privileges early + NativeMethods.EnablePrivilege(NativeMethods.SE_DEBUG_NAME); + NativeMethods.EnablePrivilege(NativeMethods.SE_PROF_SINGLE_PROCESS_NAME); + NativeMethods.EnablePrivilege(NativeMethods.SE_INC_BASE_PRIORITY_NAME); + + Logger.Info("SpdUp application starting."); + } + + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + Logger.Error("Unhandled UI exception", e.Exception); + MessageBox.Show($"An unexpected error occurred:\n{e.Exception.Message}", + "SpdUp Error", MessageBoxButton.OK, MessageBoxImage.Error); + e.Handled = true; + } + + private static void OnDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception ex) + Logger.Error("Unhandled domain exception", ex); + } + + private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + Logger.Error("Unobserved task exception", e.Exception); + e.SetObserved(); + } +} + diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Converters/Converters.cs b/Converters/Converters.cs new file mode 100644 index 0000000..1773f6f --- /dev/null +++ b/Converters/Converters.cs @@ -0,0 +1,86 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Media; + +namespace SpdUp.Converters; + +/// Bool → Visibility converter. +public sealed class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is true ? Visibility.Visible : Visibility.Collapsed; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => value is Visibility.Visible; +} + +/// Inverted Bool → Visibility converter. +public sealed class InverseBoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is true ? Visibility.Collapsed : Visibility.Visible; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => value is Visibility.Collapsed; +} + +/// Float percentage (0–100) to a progress bar width or colour. +public sealed class PercentToColorConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var pct = System.Convert.ToSingle(value); + if (pct > 90) return new SolidColorBrush(Color.FromRgb(255, 69, 58)); // red + if (pct > 70) return new SolidColorBrush(Color.FromRgb(255, 159, 10)); // orange + return new SolidColorBrush(Color.FromRgb(48, 209, 88)); // green + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} + +/// Null → Visibility (collapsed when null). +public sealed class NullToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is null ? Visibility.Collapsed : Visibility.Visible; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} + +/// Format float to one decimal. +public sealed class FloatFormatConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var fmt = parameter as string ?? "F1"; + return System.Convert.ToSingle(value).ToString(fmt); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} + +/// Bool to boost button text. +public sealed class BoostButtonTextConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is true ? "⏹ STOP BOOST" : "🚀 BOOST GAME"; + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} + +/// Bool to brush (boost active colour). +public sealed class BoostButtonColorConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is true + ? new SolidColorBrush(Color.FromRgb(255, 69, 58)) + : new SolidColorBrush(Color.FromRgb(48, 209, 88)); + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/Core/Logger.cs b/Core/Logger.cs new file mode 100644 index 0000000..cc45dfe --- /dev/null +++ b/Core/Logger.cs @@ -0,0 +1,71 @@ +using System.IO; + +namespace SpdUp.Core; + +/// +/// Application-wide logging service with live event for UI consumption. +/// +public static class Logger +{ + private static readonly object _lock = new(); + private static readonly string _logDir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SpdUp", "Logs"); + + /// Fired on every log write – subscribers receive the formatted line. + public static event Action? LogWritten; + + static Logger() + { + Directory.CreateDirectory(_logDir); + } + + private static string LogFile => Path.Combine(_logDir, $"SpdUp_{DateTime.Now:yyyy-MM-dd}.log"); + + public static void Info(string message) => Write("INFO", message); + public static void Warn(string message) => Write("WARN", message); + public static void Error(string message, Exception? ex = null) + { + var msg = ex is null ? message : $"{message} | {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}"; + Write("ERROR", msg); + } + + private static void Write(string level, string message) + { + var line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {message}"; + lock (_lock) + { + try + { + File.AppendAllText(LogFile, line + Environment.NewLine); + } + catch + { + // Swallow – logging must never crash the app. + } + } + + // Notify UI subscribers + try { LogWritten?.Invoke(line); } catch { } + +#if DEBUG + System.Diagnostics.Debug.WriteLine(line); +#endif + } + + /// + /// Removes log files older than the specified number of days. + /// + public static void PurgeOldLogs(int keepDays = 14) + { + try + { + var cutoff = DateTime.Now.AddDays(-keepDays); + foreach (var file in Directory.GetFiles(_logDir, "SpdUp_*.log")) + { + if (File.GetCreationTime(file) < cutoff) + File.Delete(file); + } + } + catch { /* best-effort */ } + } +} diff --git a/Core/NativeMethods.cs b/Core/NativeMethods.cs new file mode 100644 index 0000000..b510776 --- /dev/null +++ b/Core/NativeMethods.cs @@ -0,0 +1,153 @@ +using System.Runtime.InteropServices; + +namespace SpdUp.Core; + +/// +/// P/Invoke declarations used across the application. +/// +public static class NativeMethods +{ + // ── Memory management ────────────────────────────────────────────── + [DllImport("psapi.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EmptyWorkingSet(IntPtr hProcess); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetProcessWorkingSetSizeEx( + IntPtr hProcess, IntPtr dwMinimumWorkingSetSize, IntPtr dwMaximumWorkingSetSize, uint flags); + + // ── Timer resolution ─────────────────────────────────────────────── + [DllImport("ntdll.dll", SetLastError = true)] + public static extern int NtSetTimerResolution(uint DesiredResolution, bool SetResolution, out uint CurrentResolution); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern int NtQueryTimerResolution(out uint MinimumResolution, out uint MaximumResolution, out uint CurrentResolution); + + // ── System memory info ───────────────────────────────────────────── + [StructLayout(LayoutKind.Sequential)] + public struct MEMORYSTATUSEX + { + public uint dwLength; + public uint dwMemoryLoad; + public ulong ullTotalPhys; + public ulong ullAvailPhys; + public ulong ullTotalPageFile; + public ulong ullAvailPageFile; + public ulong ullTotalVirtual; + public ulong ullAvailVirtual; + public ulong ullAvailExtendedVirtual; + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer); + + // ── System cache / standby list purge ────────────────────────────── + public enum SYSTEM_MEMORY_LIST_COMMAND + { + MemoryCaptureAccessedBits = 0, + MemoryCaptureAndResetAccessedBits = 1, + MemoryEmptyWorkingSets = 2, + MemoryFlushModifiedList = 3, + MemoryPurgeStandbyList = 4, + MemoryPurgeLowPriorityStandbyList = 5, + MemoryCommandMax = 6 + } + + [DllImport("ntdll.dll")] + public static extern int NtSetSystemInformation( + int SystemInformationClass, ref int SystemInformation, int SystemInformationLength); + + // SystemMemoryListInformation = 0x0050 + public const int SystemMemoryListInformation = 0x0050; + + // ── Privilege ────────────────────────────────────────────────────── + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, out LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool AdjustTokenPrivileges( + IntPtr TokenHandle, bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, uint BufferLength, + IntPtr PreviousState, IntPtr ReturnLength); + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public uint LowPart; + public int HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public uint Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + public uint PrivilegeCount; + public LUID_AND_ATTRIBUTES Privileges; + } + + public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; + public const uint TOKEN_QUERY = 0x0008; + public const uint SE_PRIVILEGE_ENABLED = 0x00000002; + public const string SE_DEBUG_NAME = "SeDebugPrivilege"; + public const string SE_PROF_SINGLE_PROCESS_NAME = "SeProfileSingleProcessPrivilege"; + public const string SE_INC_BASE_PRIORITY_NAME = "SeIncreaseBasePriorityPrivilege"; + + public const uint PROCESS_ALL_ACCESS = 0x001F0FFF; + public const uint PROCESS_SET_INFORMATION = 0x0200; + public const uint PROCESS_QUERY_INFORMATION = 0x0400; + + /// + /// Enable a privilege on the current process token. + /// + public static bool EnablePrivilege(string privilege) + { + try + { + if (!OpenProcessToken(System.Diagnostics.Process.GetCurrentProcess().Handle, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out var tokenHandle)) + return false; + + try + { + if (!LookupPrivilegeValue(null, privilege, out var luid)) + return false; + + var tp = new TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new LUID_AND_ATTRIBUTES { Luid = luid, Attributes = SE_PRIVILEGE_ENABLED } + }; + + return AdjustTokenPrivileges(tokenHandle, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); + } + finally + { + CloseHandle(tokenHandle); + } + } + catch + { + return false; + } + } +} diff --git a/Core/ObservableObject.cs b/Core/ObservableObject.cs new file mode 100644 index 0000000..402fd6c --- /dev/null +++ b/Core/ObservableObject.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace SpdUp.Core; + +/// +/// Base class for ViewModels implementing INotifyPropertyChanged. +/// +public abstract class ObservableObject : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } +} diff --git a/Core/RelayCommand.cs b/Core/RelayCommand.cs new file mode 100644 index 0000000..4b19060 --- /dev/null +++ b/Core/RelayCommand.cs @@ -0,0 +1,56 @@ +using System.Windows.Input; + +namespace SpdUp.Core; + +/// +/// A basic ICommand implementation that delegates to Action/Func. +/// +public sealed class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public RelayCommand(Action execute, Func? canExecute = null) + : this(_ => execute(), canExecute is null ? null : _ => canExecute()) { } + + public event EventHandler? CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true; + public void Execute(object? parameter) => _execute(parameter); + + public void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested(); +} + +/// +/// Typed relay command. +/// +public sealed class RelayCommand : ICommand +{ + private readonly Action _execute; + private readonly Func? _canExecute; + + public RelayCommand(Action execute, Func? canExecute = null) + { + _execute = execute ?? throw new ArgumentNullException(nameof(execute)); + _canExecute = canExecute; + } + + public event EventHandler? CanExecuteChanged + { + add => CommandManager.RequerySuggested += value; + remove => CommandManager.RequerySuggested -= value; + } + + public bool CanExecute(object? parameter) => _canExecute?.Invoke((T?)parameter) ?? true; + public void Execute(object? parameter) => _execute((T?)parameter); +} diff --git a/Installer/SpdUpSetup.iss b/Installer/SpdUpSetup.iss new file mode 100644 index 0000000..7600c04 --- /dev/null +++ b/Installer/SpdUpSetup.iss @@ -0,0 +1,60 @@ +; ══════════════════════════════════════════════════════════════ +; SpdUp – Gaming Booster · Inno Setup Installer Script +; Compile with Inno Setup 6+ (https://jrsoftware.org/isinfo.php) +; ══════════════════════════════════════════════════════════════ + +#define MyAppName "SpdUp" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "Mandy" +#define MyAppExeName "SpdUp.exe" +#define PublishDir "..\publish\SpdUp" + +[Setup] +AppId={{E7A3F2C1-8D4B-4F6A-9C2E-1B5D7F3A8E4C} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +OutputDir=..\publish +OutputBaseFilename=SpdUp_Setup_{#MyAppVersion} +Compression=lzma2/ultra64 +SolidCompression=yes +LZMANumBlockThreads=4 +SetupIconFile= +UninstallDisplayIcon={app}\{#MyAppExeName} +PrivilegesRequired=admin +WizardStyle=modern +WizardSizePercent=110 +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +MinVersion=10.0.17763 +DisableWelcomePage=no +AllowNoIcons=yes +ShowLanguageDialog=auto + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "german"; MessagesFile: "compiler:Languages\German.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "startmenu"; Description: "Create Start Menu shortcut"; GroupDescription: "{cm:AdditionalIcons}" + +[Files] +; Include all published files recursively +Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startmenu +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}"; Tasks: startmenu + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent runascurrentuser + +[UninstallDelete] +; Clean up log files and settings on uninstall +Type: filesandordirs; Name: "{app}\logs" +Type: files; Name: "{app}\settings.json" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..91c0caa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Mandy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MainWindow.xaml b/MainWindow.xaml new file mode 100644 index 0000000..2084f0c --- /dev/null +++ b/MainWindow.xaml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs new file mode 100644 index 0000000..1845b95 --- /dev/null +++ b/MainWindow.xaml.cs @@ -0,0 +1,81 @@ +using System.ComponentModel; +using System.Windows; +using SpdUp.ViewModels; +using SpdUp.Views; + +namespace SpdUp; + +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window +{ + private readonly MainViewModel _vm; + private WidgetWindow? _widgetWindow; + + public MainWindow() + { + InitializeComponent(); + _vm = new MainViewModel(); + DataContext = _vm; + + _vm.PropertyChanged += OnVmPropertyChanged; + StateChanged += MainWindow_StateChanged; + + if (_vm.IsWidgetVisible) + ShowWidget(); + } + + // ── Custom title-bar handlers ───────────────────────────── + private void Minimize_Click(object sender, RoutedEventArgs e) + => WindowState = WindowState.Minimized; + + private void MaximizeRestore_Click(object sender, RoutedEventArgs e) + => WindowState = WindowState == WindowState.Maximized + ? WindowState.Normal + : WindowState.Maximized; + + private void Close_Click(object sender, RoutedEventArgs e) + => Close(); + + private void MainWindow_StateChanged(object? sender, System.EventArgs e) + { + // Update the maximize/restore glyph + if (MaxRestoreBtn is not null) + MaxRestoreBtn.Content = WindowState == WindowState.Maximized ? "❐" : "☐"; + } + + private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(MainViewModel.IsWidgetVisible)) + { + if (_vm.IsWidgetVisible) + ShowWidget(); + else + HideWidget(); + } + } + + private void ShowWidget() + { + if (_widgetWindow is null || !_widgetWindow.IsLoaded) + { + _widgetWindow = new WidgetWindow(_vm); + _widgetWindow.Closed += (_, _) => _vm.IsWidgetVisible = false; + _widgetWindow.Show(); + } + } + + private void HideWidget() + { + _widgetWindow?.Close(); + _widgetWindow = null; + } + + private void Window_Closing(object sender, CancelEventArgs e) + { + HideWidget(); + _vm.SaveSettings(); + _vm.Dispose(); + } +} \ No newline at end of file diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs new file mode 100644 index 0000000..302e5b3 --- /dev/null +++ b/Models/AppSettings.cs @@ -0,0 +1,77 @@ +using System.IO; +using System.Text.Json; + +namespace SpdUp.Models; + +/// +/// Persisted application settings. +/// +public sealed class AppSettings +{ + private static readonly string _filePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "SpdUp", "settings.json"); + + // ── General ──────────────────────────────────────────────────────── + public bool MinimizeToTray { get; set; } = true; + public bool StartWithWindows { get; set; } = false; + public bool AutoDetectGames { get; set; } = true; + public bool AutoBoostOnGameLaunch { get; set; } = true; + + // ── Widget ───────────────────────────────────────────────────────── + public bool ShowWidget { get; set; } = false; + public bool WidgetCompactMode { get; set; } = true; + public double WidgetLeft { get; set; } = 100; + public double WidgetTop { get; set; } = 100; + public double WidgetOpacity { get; set; } = 0.85; + public int WidgetUpdateIntervalMs { get; set; } = 1000; + public bool WidgetClickThrough { get; set; } = false; + + // ── Monitoring ───────────────────────────────────────────────────── + public int MonitoringIntervalMs { get; set; } = 1000; + + // ── Network ──────────────────────────────────────────────────────── + public string PingTarget { get; set; } = "8.8.8.8"; + public string PreferredDns { get; set; } = "1.1.1.1"; + + // ── Game Library ─────────────────────────────────────────────────── + public List Games { get; set; } = new(); + + // ── Boost services to stop ───────────────────────────────────────── + public List ServicesToStop { get; set; } = new() + { + "SysMain", "WSearch", "DiagTrack", "TabletInputService", "MapsBroker", + "PcaSvc", "wisvc" + }; + + // ── Persistence ──────────────────────────────────────────────────── + private static readonly JsonSerializerOptions _jsonOpts = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + public static AppSettings Load() + { + try + { + if (File.Exists(_filePath)) + { + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json, _jsonOpts) ?? new AppSettings(); + } + } + catch { /* return default */ } + return new AppSettings(); + } + + public void Save() + { + try + { + var dir = Path.GetDirectoryName(_filePath)!; + Directory.CreateDirectory(dir); + File.WriteAllText(_filePath, JsonSerializer.Serialize(this, _jsonOpts)); + } + catch { /* best-effort */ } + } +} diff --git a/Models/GameEntry.cs b/Models/GameEntry.cs new file mode 100644 index 0000000..3fcc51b --- /dev/null +++ b/Models/GameEntry.cs @@ -0,0 +1,33 @@ +using System.IO; + +namespace SpdUp.Models; + +/// +/// Represents a detected or manually-added game. +/// +public sealed class GameEntry +{ + public string Name { get; set; } = string.Empty; + public string ExecutablePath { get; set; } = string.Empty; + public string ProcessName { get; set; } = string.Empty; + public string LauncherSource { get; set; } = "Manual"; + public string? IconPath { get; set; } + public bool AutoBoost { get; set; } = true; + public BoostProfile Profile { get; set; } = new(); + + /// Derive the process name from the executable path. + public static string ProcessNameFromPath(string exePath) + => Path.GetFileNameWithoutExtension(exePath); +} + +/// +/// Per-game boost profile overrides. +/// +public sealed class BoostProfile +{ + public bool CleanRam { get; set; } = true; + public bool SetHighPriority { get; set; } = true; + public bool ReduceTimerResolution { get; set; } = true; + public bool StopServices { get; set; } = true; + public bool OptimizeNetwork { get; set; } = false; +} diff --git a/Models/ProcessEntry.cs b/Models/ProcessEntry.cs new file mode 100644 index 0000000..8645111 --- /dev/null +++ b/Models/ProcessEntry.cs @@ -0,0 +1,15 @@ +namespace SpdUp.Models; + +/// +/// Represents a lightweight process view for the Process Manager. +/// +public sealed class ProcessEntry +{ + public int Pid { get; init; } + public string Name { get; init; } = string.Empty; + public string? WindowTitle { get; init; } + public float CpuPercent { get; set; } + public float MemoryMB { get; set; } + public string PriorityClass { get; set; } = "Normal"; + public bool IsSystem { get; init; } +} diff --git a/Models/ServiceEntry.cs b/Models/ServiceEntry.cs new file mode 100644 index 0000000..074f9b7 --- /dev/null +++ b/Models/ServiceEntry.cs @@ -0,0 +1,13 @@ +namespace SpdUp.Models; + +/// +/// Represents a Windows service relevant to the boost process. +/// +public sealed class ServiceEntry +{ + public string ServiceName { get; init; } = string.Empty; + public string DisplayName { get; init; } = string.Empty; + public string Status { get; set; } = "Unknown"; + public bool WasRunningBeforeBoost { get; set; } + public string Description { get; init; } = string.Empty; +} diff --git a/Models/SystemMetrics.cs b/Models/SystemMetrics.cs new file mode 100644 index 0000000..4312fe4 --- /dev/null +++ b/Models/SystemMetrics.cs @@ -0,0 +1,18 @@ +namespace SpdUp.Models; + +/// +/// Snapshot of current system performance metrics. +/// +public sealed class SystemMetrics +{ + public float CpuUsage { get; init; } + public float RamUsageMB { get; init; } + public float RamTotalMB { get; init; } + public float RamUsagePercent => RamTotalMB > 0 ? (RamUsageMB / RamTotalMB) * 100f : 0f; + public float GpuUsage { get; init; } + public float CpuTemperature { get; init; } + public float GpuTemperature { get; init; } + public float NetworkPingMs { get; init; } + public float Fps { get; init; } + public DateTime Timestamp { get; init; } = DateTime.Now; +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bff6cc --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# ⚡ SpdUp – Gaming Booster for Windows + + + + + + + + + + + A free, open-source Windows gaming optimizer that boosts your PC's performance with a single click. + + +--- + +## 🚀 Features + +| Feature | Description | +|---|---| +| **One-Click Boost** | CPU priority, RAM cleanup, timer resolution, GPU power profiles, network tuning, and service management — all in one click | +| **RAM Optimizer** | Deep working-set trimming + Windows Standby List purge via kernel-level calls | +| **Real-Time Monitor** | Live CPU, GPU, RAM, temperatures, fan speeds, and network latency via LibreHardwareMonitor | +| **Network Optimizer** | TCP registry tweaks (Nagle's algorithm, ACK frequency, auto-tuning) for low-latency gaming | +| **Service Manager** | Identifies and safely disables non-essential Windows services during gaming | +| **Game Detection** | Auto-detects 30+ popular games (Steam, Epic, Riot, Blizzard, EA) and applies boost automatically | +| **Shader Cache Cleaner** | Clears DirectX, NVIDIA, AMD, and Intel shader caches to fix stuttering | +| **Process Manager** | View and manage processes with CPU priority and affinity controls | +| **Desktop Widget** | Always-on-top floating overlay with live metrics and quick-boost button | +| **Debug Log** | Built-in live log viewer with filtering, copy, and file logging | + +## 📸 Screenshots + + + Dark gaming UI with custom window chrome, sidebar navigation, and live system metrics + + +> Add your screenshots to the `website/screenshots/` folder and reference them here. + +## 🏗️ Tech Stack + +- **.NET 8** — Latest runtime, self-contained deployment (no framework install needed) +- **WPF** — Windows Presentation Foundation with full MVVM architecture +- **LibreHardwareMonitor** — Accurate CPU/GPU temps, fan speeds, and clock readings +- **P/Invoke** — Kernel-level calls for RAM cleanup, timer resolution, and privilege elevation +- **Custom Dark Theme** — Styled scrollbars, buttons, data grids, and window chrome +- **Inno Setup** — Single-file installer with LZMA2 compression + +## 📦 Installation + +### Option 1: Installer (Recommended) +1. Download `SpdUp_Setup_1.0.0.exe` from [Releases](../../releases) +2. Run the installer +3. Launch SpdUp from the Start Menu or Desktop + +### Option 2: Build from Source +```bash +git clone https://github.com/immissmandy/SpdUp.git +cd SpdUp +dotnet build SpdUp.csproj -c Release +``` + +### Build the Installer +```powershell +# Publish self-contained +.\build-installer.ps1 + +# Publish + compile Inno Setup installer +.\build-installer.ps1 -Installer +``` + +> Requires [Inno Setup 6](https://jrsoftware.org/isdl.php) for the `-Installer` flag. + +## 🧪 Tests + +```bash +dotnet test SpdUp.Tests +``` + +**84 unit tests** covering: +- Core infrastructure (ObservableObject, RelayCommand, Logger) +- All model classes and computed properties +- WPF value converters (7 converters, 13 tests with Theory data) +- Service layer (Network, GameDetection, Optimization, ServiceManagement) +- ViewModel logic (DebugLogViewModel with STA thread support) + +## 📁 Project Structure + +``` +SpdUp/ +├── Core/ # ObservableObject, RelayCommand, Logger, NativeMethods (P/Invoke) +├── Models/ # AppSettings, GameEntry, ProcessEntry, ServiceEntry, SystemMetrics +├── Services/ # Optimization, RAM, Network, GameDetection, Monitoring, ShaderCache +├── ViewModels/ # MVVM ViewModels for each page +├── Views/ # WPF XAML Views + code-behind +├── Themes/ # DarkTheme.xaml (full custom dark gaming theme) +├── Converters/ # WPF value converters +├── Installer/ # Inno Setup script (SpdUpSetup.iss) +├── SpdUp.Tests/ # xUnit test project (84 tests) +├── website/ # Landing page (index.html) +├── build-installer.ps1 # Build + package script +└── SpdUp.csproj # .NET 8 WPF project +``` + +## ⚙️ How It Works + +1. **Install & Launch** — SpdUp runs with administrator privileges for deep system access +2. **Hit Boost** — Optimizes CPU, RAM, GPU, network, timer resolution, and disables unnecessary services +3. **Game On** — Play with maximum performance; click Restore when done to revert all changes + +All changes are **fully reversible**. SpdUp saves previous states and restores them on deactivation. + +## 💻 System Requirements + +| Requirement | Minimum | +|---|---| +| OS | Windows 10/11 (64-bit, version 1809+) | +| RAM | 4 GB (8 GB recommended) | +| Disk Space | ~200 MB | +| Privileges | Administrator (for RAM cleanup, service control, timer resolution) | + +## 🏳️⚧️ Made with ❤️ by Mandy + + + + + +## 📄 License + +This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details. diff --git a/Services/GameDetectionService.cs b/Services/GameDetectionService.cs new file mode 100644 index 0000000..3c2cfe2 --- /dev/null +++ b/Services/GameDetectionService.cs @@ -0,0 +1,157 @@ +using System.Diagnostics; +using SpdUp.Core; +using SpdUp.Models; + +namespace SpdUp.Services; + +/// +/// Detects running games and known launchers. +/// +public sealed class GameDetectionService : IDisposable +{ + private readonly Timer _pollTimer; + private readonly AppSettings _settings; + private readonly HashSet _knownGameProcesses = new(StringComparer.OrdinalIgnoreCase); + private string? _detectedGameProcess; + + public event Action? GameStarted; + public event Action? GameStopped; + + /// + /// Well-known launcher processes that indicate a game may launch. + /// + private static readonly HashSet LauncherProcesses = new(StringComparer.OrdinalIgnoreCase) + { + "steam", "steamwebhelper", + "EpicGamesLauncher", + "Battle.net", "Agent" + }; + + /// + /// Heuristic: processes that look like games (high-memory GPU-using apps not in system dirs). + /// + private static readonly HashSet 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(); + } +} diff --git a/Services/MonitoringService.cs b/Services/MonitoringService.cs new file mode 100644 index 0000000..3a24cb7 --- /dev/null +++ b/Services/MonitoringService.cs @@ -0,0 +1,157 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using LibreHardwareMonitor.Hardware; +using SpdUp.Core; +using SpdUp.Models; + +namespace SpdUp.Services; + +/// +/// Live system monitoring using performance counters + LibreHardwareMonitor. +/// Designed for low overhead – one shared timer drives all consumers. +/// +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? 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.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 { } + } +} diff --git a/Services/NetworkService.cs b/Services/NetworkService.cs new file mode 100644 index 0000000..5d9fac1 --- /dev/null +++ b/Services/NetworkService.cs @@ -0,0 +1,107 @@ +using System.Net.NetworkInformation; +using Microsoft.Win32; +using SpdUp.Core; + +namespace SpdUp.Services; + +/// +/// Network tweaks for gaming: DNS, TCP settings, latency checks. +/// All changes are reversible via Restore methods. +/// +public sealed class NetworkService +{ + private readonly Dictionary _previousRegValues = new(); + + /// + /// Measure round-trip ping to a host. + /// + public static async Task 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; + } + } + + /// + /// Apply TCP gaming optimisations via the registry (Nagle, TCP ack frequency, etc.). + /// + 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); + } + } + + /// + /// Restore all TCP settings to their previous values. + /// + 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); + } +} diff --git a/Services/OptimizationService.cs b/Services/OptimizationService.cs new file mode 100644 index 0000000..18e5161 --- /dev/null +++ b/Services/OptimizationService.cs @@ -0,0 +1,208 @@ +using System.Diagnostics; +using System.ServiceProcess; +using SpdUp.Core; +using SpdUp.Models; + +namespace SpdUp.Services; + +/// +/// Manages the full boost lifecycle: activate → monitor → restore. +/// +public sealed class OptimizationService +{ + private readonly RamOptimizationService _ramService = new(); + private readonly AppSettings _settings; + + private readonly List _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? BoostActivated; + public event Action? BoostDeactivated; + + public OptimizationService(AppSettings settings) + { + _settings = settings; + } + + /// + /// Activate all boost optimizations. Thread-safe. + /// + public async Task ActivateBoostAsync(string? gameProcessName = null) + { + if (_isBoosted) return new BoostResult(false, "Boost is already active."); + + var messages = new List(); + + 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}"); + } + } + + /// + /// Restore all changes made during boost. + /// + 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); + } + } + + /// + /// Set priority class for a process by name. + /// + 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(); + } + } + } + + /// + /// Kill a process by PID. + /// + 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); diff --git a/Services/RamOptimizationService.cs b/Services/RamOptimizationService.cs new file mode 100644 index 0000000..936cddb --- /dev/null +++ b/Services/RamOptimizationService.cs @@ -0,0 +1,111 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using SpdUp.Core; + +namespace SpdUp.Services; + +/// +/// Performs real memory optimizations: working-set trimming + standby-list purge. +/// Requires administrator privileges. +/// +public sealed class RamOptimizationService +{ + /// + /// Trim working sets of all accessible processes – pushes unused pages to the standby list. + /// + 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); + } + + /// + /// Purge the standby memory list. Requires SeProfileSingleProcessPrivilege. + /// + 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()); + + 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; + } + } + + /// + /// Full RAM optimisation: trim working sets + purge standby. + /// + public RamCleanupResult FullCleanup() + { + var result = TrimWorkingSets(); + var purged = PurgeStandbyList(); + return result with { StandbyPurged = purged ? 1 : 0 }; + } + + /// + /// Returns current memory statistics. + /// + public static (ulong totalMB, ulong availMB, uint loadPercent) GetMemoryStatus() + { + var mem = new NativeMethods.MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf() }; + NativeMethods.GlobalMemoryStatusEx(ref mem); + return (mem.ullTotalPhys / 1048576, mem.ullAvailPhys / 1048576, mem.dwMemoryLoad); + } +} + +public record struct RamCleanupResult(int TrimmedCount, int FailedCount, int StandbyPurged); diff --git a/Services/ServiceManagementService.cs b/Services/ServiceManagementService.cs new file mode 100644 index 0000000..4a11cd8 --- /dev/null +++ b/Services/ServiceManagementService.cs @@ -0,0 +1,96 @@ +using System.Diagnostics; +using System.ServiceProcess; +using SpdUp.Core; +using SpdUp.Models; + +namespace SpdUp.Services; + +/// +/// Service management: list, stop, start Windows services. +/// +public static class ServiceManagementService +{ + /// + /// Non-critical services commonly stopped for gaming. + /// + public static readonly Dictionary 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 GetServiceList(IEnumerable serviceNames) + { + var entries = new List(); + + 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; + } +} diff --git a/Services/ShaderCacheCleanerService.cs b/Services/ShaderCacheCleanerService.cs new file mode 100644 index 0000000..c3da211 --- /dev/null +++ b/Services/ShaderCacheCleanerService.cs @@ -0,0 +1,110 @@ +using System.IO; +using SpdUp.Core; + +namespace SpdUp.Services; + +/// +/// Cleans DirectX shader cache, NVIDIA shader cache, and temp files +/// to reduce micro-stuttering in games. +/// +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; +} diff --git a/SpdUp.Tests/ConverterTests.cs b/SpdUp.Tests/ConverterTests.cs new file mode 100644 index 0000000..c47a220 --- /dev/null +++ b/SpdUp.Tests/ConverterTests.cs @@ -0,0 +1,175 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Media; +using SpdUp.Converters; + +namespace SpdUp.Tests; + +/// +/// Tests for WPF value converters. +/// +public class ConverterTests +{ + private static readonly CultureInfo Culture = CultureInfo.InvariantCulture; + + // ── BoolToVisibilityConverter ───────────────────────────── + + [Fact] + public void BoolToVisibility_True_ReturnsVisible() + { + var conv = new BoolToVisibilityConverter(); + var result = conv.Convert(true, typeof(Visibility), null!, Culture); + Assert.Equal(Visibility.Visible, result); + } + + [Fact] + public void BoolToVisibility_False_ReturnsCollapsed() + { + var conv = new BoolToVisibilityConverter(); + var result = conv.Convert(false, typeof(Visibility), null!, Culture); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void BoolToVisibility_ConvertBack_Visible_ReturnsTrue() + { + var conv = new BoolToVisibilityConverter(); + var result = conv.ConvertBack(Visibility.Visible, typeof(bool), null!, Culture); + Assert.Equal(true, result); + } + + [Fact] + public void BoolToVisibility_ConvertBack_Collapsed_ReturnsFalse() + { + var conv = new BoolToVisibilityConverter(); + var result = conv.ConvertBack(Visibility.Collapsed, typeof(bool), null!, Culture); + Assert.Equal(false, result); + } + + // ── InverseBoolToVisibilityConverter ────────────────────── + + [Fact] + public void InverseBoolToVisibility_True_ReturnsCollapsed() + { + var conv = new InverseBoolToVisibilityConverter(); + var result = conv.Convert(true, typeof(Visibility), null!, Culture); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void InverseBoolToVisibility_False_ReturnsVisible() + { + var conv = new InverseBoolToVisibilityConverter(); + var result = conv.Convert(false, typeof(Visibility), null!, Culture); + Assert.Equal(Visibility.Visible, result); + } + + // ── PercentToColorConverter ────────────────────────────── + + [Theory] + [InlineData(95f, 255, 69, 58)] // red for > 90 + [InlineData(91f, 255, 69, 58)] // red at boundary + [InlineData(80f, 255, 159, 10)] // orange for > 70 + [InlineData(71f, 255, 159, 10)] // orange at boundary + [InlineData(50f, 48, 209, 88)] // green for <= 70 + [InlineData(0f, 48, 209, 88)] // green at zero + public void PercentToColor_ReturnsCorrectColor(float pct, byte r, byte g, byte b) + { + var conv = new PercentToColorConverter(); + var result = conv.Convert(pct, typeof(Brush), null!, Culture) as SolidColorBrush; + + Assert.NotNull(result); + Assert.Equal(r, result.Color.R); + Assert.Equal(g, result.Color.G); + Assert.Equal(b, result.Color.B); + } + + [Fact] + public void PercentToColor_ConvertBack_Throws() + { + var conv = new PercentToColorConverter(); + Assert.Throws(() => + conv.ConvertBack(null!, typeof(float), null!, Culture)); + } + + // ── NullToVisibilityConverter ──────────────────────────── + + [Fact] + public void NullToVisibility_Null_ReturnsCollapsed() + { + var conv = new NullToVisibilityConverter(); + var result = conv.Convert(null!, typeof(Visibility), null!, Culture); + Assert.Equal(Visibility.Collapsed, result); + } + + [Fact] + public void NullToVisibility_NotNull_ReturnsVisible() + { + var conv = new NullToVisibilityConverter(); + var result = conv.Convert("something", typeof(Visibility), null!, Culture); + Assert.Equal(Visibility.Visible, result); + } + + // ── FloatFormatConverter ───────────────────────────────── + + [Fact] + public void FloatFormat_DefaultFormat_OneDecimal() + { + var conv = new FloatFormatConverter(); + var result = (string)conv.Convert(42.567f, typeof(string), null!, CultureInfo.InvariantCulture); + // Converter uses current culture internally, so parse back to float + Assert.True(float.TryParse(result, System.Globalization.NumberStyles.Float, CultureInfo.CurrentCulture, out var parsed)); + Assert.Equal(42.6f, parsed, 0.05f); + } + + [Fact] + public void FloatFormat_CustomFormat() + { + var conv = new FloatFormatConverter(); + var result = (string)conv.Convert(42.567f, typeof(string), "F2", CultureInfo.InvariantCulture); + Assert.True(float.TryParse(result, System.Globalization.NumberStyles.Float, CultureInfo.CurrentCulture, out var parsed)); + Assert.Equal(42.57f, parsed, 0.005f); + } + + // ── BoostButtonTextConverter ───────────────────────────── + + [Fact] + public void BoostButtonText_True_ReturnsStopText() + { + var conv = new BoostButtonTextConverter(); + var result = conv.Convert(true, typeof(string), null!, Culture); + Assert.Contains("STOP", result as string ?? ""); + } + + [Fact] + public void BoostButtonText_False_ReturnsBoostText() + { + var conv = new BoostButtonTextConverter(); + var result = conv.Convert(false, typeof(string), null!, Culture); + Assert.Contains("BOOST", result as string ?? ""); + } + + // ── BoostButtonColorConverter ──────────────────────────── + + [Fact] + public void BoostButtonColor_True_ReturnsRed() + { + var conv = new BoostButtonColorConverter(); + var result = conv.Convert(true, typeof(Brush), null!, Culture) as SolidColorBrush; + + Assert.NotNull(result); + Assert.Equal(255, result.Color.R); + Assert.Equal(69, result.Color.G); + } + + [Fact] + public void BoostButtonColor_False_ReturnsGreen() + { + var conv = new BoostButtonColorConverter(); + var result = conv.Convert(false, typeof(Brush), null!, Culture) as SolidColorBrush; + + Assert.NotNull(result); + Assert.Equal(48, result.Color.R); + Assert.Equal(209, result.Color.G); + } +} diff --git a/SpdUp.Tests/GlobalUsings.cs b/SpdUp.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/SpdUp.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/SpdUp.Tests/LoggerTests.cs b/SpdUp.Tests/LoggerTests.cs new file mode 100644 index 0000000..6924ab5 --- /dev/null +++ b/SpdUp.Tests/LoggerTests.cs @@ -0,0 +1,117 @@ +using SpdUp.Core; + +namespace SpdUp.Tests; + +/// +/// Tests for the Logger class. +/// +public class LoggerTests +{ + [Fact] + public void Info_FiresLogWrittenEvent() + { + string? received = null; + Logger.LogWritten += handler; + + try + { + Logger.Info("test info message"); + Assert.NotNull(received); + Assert.Contains("[INFO]", received); + Assert.Contains("test info message", received); + } + finally + { + Logger.LogWritten -= handler; + } + + void handler(string line) => received = line; + } + + [Fact] + public void Warn_FiresLogWrittenEventWithWarnLevel() + { + string? received = null; + Logger.LogWritten += handler; + + try + { + Logger.Warn("test warning"); + Assert.NotNull(received); + Assert.Contains("[WARN]", received); + Assert.Contains("test warning", received); + } + finally + { + Logger.LogWritten -= handler; + } + + void handler(string line) => received = line; + } + + [Fact] + public void Error_IncludesExceptionDetails() + { + string? received = null; + Logger.LogWritten += handler; + + try + { + Logger.Error("something failed", new InvalidOperationException("bad state")); + Assert.NotNull(received); + Assert.Contains("[ERROR]", received); + Assert.Contains("something failed", received); + Assert.Contains("InvalidOperationException", received); + Assert.Contains("bad state", received); + } + finally + { + Logger.LogWritten -= handler; + } + + void handler(string line) => received = line; + } + + [Fact] + public void Error_WithoutException_DoesNotContainPipe() + { + string? received = null; + Logger.LogWritten += handler; + + try + { + Logger.Error("simple error"); + Assert.NotNull(received); + Assert.Contains("[ERROR]", received); + Assert.Contains("simple error", received); + Assert.DoesNotContain("|", received); + } + finally + { + Logger.LogWritten -= handler; + } + + void handler(string line) => received = line; + } + + [Fact] + public void LogWritten_IncludesTimestamp() + { + string? received = null; + Logger.LogWritten += handler; + + try + { + Logger.Info("timestamp check"); + Assert.NotNull(received); + // Format: [yyyy-MM-dd HH:mm:ss.fff] + Assert.Matches(@"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]", received); + } + finally + { + Logger.LogWritten -= handler; + } + + void handler(string line) => received = line; + } +} diff --git a/SpdUp.Tests/ModelTests.cs b/SpdUp.Tests/ModelTests.cs new file mode 100644 index 0000000..c05c4b1 --- /dev/null +++ b/SpdUp.Tests/ModelTests.cs @@ -0,0 +1,224 @@ +using SpdUp.Models; + +namespace SpdUp.Tests; + +/// +/// Tests for the model classes. +/// +public class ModelTests +{ + // ── SystemMetrics ──────────────────────────────────────── + + [Fact] + public void SystemMetrics_RamUsagePercent_CalculatesCorrectly() + { + var m = new SystemMetrics { RamUsageMB = 8000, RamTotalMB = 16000 }; + Assert.Equal(50f, m.RamUsagePercent); + } + + [Fact] + public void SystemMetrics_RamUsagePercent_ZeroTotal_ReturnsZero() + { + var m = new SystemMetrics { RamUsageMB = 100, RamTotalMB = 0 }; + Assert.Equal(0f, m.RamUsagePercent); + } + + [Fact] + public void SystemMetrics_RamUsagePercent_FullRam() + { + var m = new SystemMetrics { RamUsageMB = 16000, RamTotalMB = 16000 }; + Assert.Equal(100f, m.RamUsagePercent); + } + + [Fact] + public void SystemMetrics_DefaultTimestamp_IsNow() + { + var before = DateTime.Now.AddSeconds(-1); + var m = new SystemMetrics(); + var after = DateTime.Now.AddSeconds(1); + + Assert.InRange(m.Timestamp, before, after); + } + + // ── GameEntry ──────────────────────────────────────────── + + [Fact] + public void GameEntry_ProcessNameFromPath_ExtractsFileName() + { + var result = GameEntry.ProcessNameFromPath(@"C:\Games\Cyberpunk2077\bin\Cyberpunk2077.exe"); + Assert.Equal("Cyberpunk2077", result); + } + + [Fact] + public void GameEntry_ProcessNameFromPath_HandlesForwardSlashes() + { + var result = GameEntry.ProcessNameFromPath("C:/Games/Game.exe"); + Assert.Equal("Game", result); + } + + [Fact] + public void GameEntry_ProcessNameFromPath_JustFileName() + { + var result = GameEntry.ProcessNameFromPath("myapp.exe"); + Assert.Equal("myapp", result); + } + + [Fact] + public void GameEntry_DefaultValues() + { + var g = new GameEntry(); + Assert.Equal(string.Empty, g.Name); + Assert.Equal(string.Empty, g.ExecutablePath); + Assert.Equal("Manual", g.LauncherSource); + Assert.True(g.AutoBoost); + Assert.Null(g.IconPath); + } + + [Fact] + public void BoostProfile_DefaultValues() + { + var p = new BoostProfile(); + Assert.True(p.CleanRam); + Assert.True(p.SetHighPriority); + Assert.True(p.ReduceTimerResolution); + Assert.True(p.StopServices); + Assert.False(p.OptimizeNetwork); + } + + // ── ProcessEntry ───────────────────────────────────────── + + [Fact] + public void ProcessEntry_Properties() + { + var p = new ProcessEntry + { + Pid = 1234, + Name = "chrome", + WindowTitle = "Google Chrome", + CpuPercent = 12.5f, + MemoryMB = 350f, + PriorityClass = "High", + IsSystem = false + }; + + Assert.Equal(1234, p.Pid); + Assert.Equal("chrome", p.Name); + Assert.Equal("Google Chrome", p.WindowTitle); + Assert.Equal(12.5f, p.CpuPercent); + Assert.Equal(350f, p.MemoryMB); + Assert.Equal("High", p.PriorityClass); + Assert.False(p.IsSystem); + } + + [Fact] + public void ProcessEntry_DefaultPriority_IsNormal() + { + var p = new ProcessEntry { Pid = 1, Name = "test" }; + Assert.Equal("Normal", p.PriorityClass); + } + + // ── ServiceEntry ───────────────────────────────────────── + + [Fact] + public void ServiceEntry_DefaultStatus_IsUnknown() + { + var s = new ServiceEntry(); + Assert.Equal("Unknown", s.Status); + Assert.False(s.WasRunningBeforeBoost); + } + + [Fact] + public void ServiceEntry_Properties() + { + var s = new ServiceEntry + { + ServiceName = "WSearch", + DisplayName = "Windows Search", + Status = "Running", + WasRunningBeforeBoost = true, + Description = "Full-text search" + }; + + Assert.Equal("WSearch", s.ServiceName); + Assert.Equal("Windows Search", s.DisplayName); + Assert.Equal("Running", s.Status); + Assert.True(s.WasRunningBeforeBoost); + } + + // ── AppSettings ────────────────────────────────────────── + + [Fact] + public void AppSettings_DefaultValues() + { + var s = new AppSettings(); + Assert.True(s.MinimizeToTray); + Assert.False(s.StartWithWindows); + Assert.True(s.AutoDetectGames); + Assert.True(s.AutoBoostOnGameLaunch); + Assert.False(s.ShowWidget); + Assert.Equal("8.8.8.8", s.PingTarget); + Assert.Equal("1.1.1.1", s.PreferredDns); + Assert.Equal(1000, s.MonitoringIntervalMs); + Assert.Contains("SysMain", s.ServicesToStop); + Assert.Contains("WSearch", s.ServicesToStop); + } + + [Fact] + public void AppSettings_WidgetDefaults() + { + var s = new AppSettings(); + Assert.True(s.WidgetCompactMode); + Assert.Equal(100, s.WidgetLeft); + Assert.Equal(100, s.WidgetTop); + Assert.Equal(0.85, s.WidgetOpacity); + Assert.Equal(1000, s.WidgetUpdateIntervalMs); + Assert.False(s.WidgetClickThrough); + } + + // ── ShaderCleanResult ──────────────────────────────────── + + [Fact] + public void ShaderCleanResult_MBFreed_Calculation() + { + var r = new SpdUp.Services.ShaderCleanResult(10, 10_485_760); // 10 MB + Assert.Equal(10.0, r.MBFreed, 1); + } + + [Fact] + public void ShaderCleanResult_ZeroBytes() + { + var r = new SpdUp.Services.ShaderCleanResult(0, 0); + Assert.Equal(0.0, r.MBFreed); + Assert.Equal(0, r.FilesDeleted); + } + + // ── RamCleanupResult ───────────────────────────────────── + + [Fact] + public void RamCleanupResult_RecordWithSyntax() + { + var r = new SpdUp.Services.RamCleanupResult(50, 5, 0); + var r2 = r with { StandbyPurged = 1 }; + + Assert.Equal(50, r2.TrimmedCount); + Assert.Equal(5, r2.FailedCount); + Assert.Equal(1, r2.StandbyPurged); + } + + // ── BoostResult ────────────────────────────────────────── + + [Fact] + public void BoostResult_Properties() + { + var r = new SpdUp.Services.BoostResult(true, "Boost activated"); + Assert.True(r.Success); + Assert.Equal("Boost activated", r.Message); + } + + [Fact] + public void BoostResult_Failure() + { + var r = new SpdUp.Services.BoostResult(false, "Error occurred"); + Assert.False(r.Success); + } +} diff --git a/SpdUp.Tests/ObservableObjectTests.cs b/SpdUp.Tests/ObservableObjectTests.cs new file mode 100644 index 0000000..267eb6d --- /dev/null +++ b/SpdUp.Tests/ObservableObjectTests.cs @@ -0,0 +1,93 @@ +using System.ComponentModel; +using SpdUp.Core; + +namespace SpdUp.Tests; + +/// +/// Tests for ObservableObject base class. +/// +public class ObservableObjectTests +{ + private sealed class TestObservable : ObservableObject + { + private string _name = string.Empty; + public string Name + { + get => _name; + set => SetProperty(ref _name, value); + } + + private int _count; + public int Count + { + get => _count; + set => SetProperty(ref _count, value); + } + } + + [Fact] + public void SetProperty_NewValue_RaisesPropertyChanged() + { + var obj = new TestObservable(); + string? changedProp = null; + obj.PropertyChanged += (_, e) => changedProp = e.PropertyName; + + obj.Name = "hello"; + + Assert.Equal("Name", changedProp); + Assert.Equal("hello", obj.Name); + } + + [Fact] + public void SetProperty_SameValue_DoesNotRaisePropertyChanged() + { + var obj = new TestObservable { Name = "hello" }; + bool raised = false; + obj.PropertyChanged += (_, _) => raised = true; + + obj.Name = "hello"; + + Assert.False(raised); + } + + [Fact] + public void SetProperty_ReturnsTrue_WhenValueChanged() + { + var obj = new TestObservable(); + string? changedProp = null; + obj.PropertyChanged += (_, e) => changedProp = e.PropertyName; + + obj.Count = 42; + + Assert.Equal("Count", changedProp); + Assert.Equal(42, obj.Count); + } + + [Fact] + public void SetProperty_ReturnsFalse_WhenValueUnchanged() + { + var obj = new TestObservable { Count = 5 }; + bool raised = false; + obj.PropertyChanged += (_, _) => raised = true; + + obj.Count = 5; + + Assert.False(raised); + } + + [Fact] + public void MultipleProperties_OnlyNotifyChanged() + { + var obj = new TestObservable(); + var notifications = new List(); + obj.PropertyChanged += (_, e) => notifications.Add(e.PropertyName!); + + obj.Name = "a"; + obj.Count = 1; + obj.Name = "a"; // same – no notification + obj.Count = 2; + + Assert.Equal(3, notifications.Count); + Assert.Equal(["Name", "Count", "Count"], notifications); + } +} diff --git a/SpdUp.Tests/RelayCommandTests.cs b/SpdUp.Tests/RelayCommandTests.cs new file mode 100644 index 0000000..937a2b8 --- /dev/null +++ b/SpdUp.Tests/RelayCommandTests.cs @@ -0,0 +1,99 @@ +using SpdUp.Core; + +namespace SpdUp.Tests; + +/// +/// Tests for RelayCommand and RelayCommand<T>. +/// +public class RelayCommandTests +{ + [Fact] + public void Execute_InvokesAction() + { + bool executed = false; + var cmd = new RelayCommand(() => executed = true); + + cmd.Execute(null); + + Assert.True(executed); + } + + [Fact] + public void Execute_WithParameter_InvokesParameterizedAction() + { + object? received = null; + var cmd = new RelayCommand(p => received = p); + + cmd.Execute("test"); + + Assert.Equal("test", received); + } + + [Fact] + public void CanExecute_DefaultsToTrue() + { + var cmd = new RelayCommand(() => { }); + + Assert.True(cmd.CanExecute(null)); + } + + [Fact] + public void CanExecute_RespectsCanExecuteFunc() + { + var cmd = new RelayCommand(() => { }, () => false); + + Assert.False(cmd.CanExecute(null)); + } + + [Fact] + public void CanExecute_ParameterizedPredicate() + { + var cmd = new RelayCommand(_ => { }, p => p is "go"); + + Assert.True(cmd.CanExecute("go")); + Assert.False(cmd.CanExecute("stop")); + Assert.False(cmd.CanExecute(null)); + } + + [Fact] + public void Constructor_ThrowsOnNullAction() + { + Assert.Throws(() => new RelayCommand((Action)null!)); + } + + // ── Generic RelayCommand ────────────────────────────── + + [Fact] + public void Generic_Execute_InvokesWithTypedParameter() + { + string? received = null; + var cmd = new RelayCommand(s => received = s); + + cmd.Execute("hello"); + + Assert.Equal("hello", received); + } + + [Fact] + public void Generic_CanExecute_DefaultsToTrue() + { + var cmd = new RelayCommand(_ => { }); + + Assert.True(cmd.CanExecute(42)); + } + + [Fact] + public void Generic_CanExecute_RespectsFunc() + { + var cmd = new RelayCommand(_ => { }, n => n > 0); + + Assert.True(cmd.CanExecute(5)); + Assert.False(cmd.CanExecute(-1)); + } + + [Fact] + public void Generic_Constructor_ThrowsOnNullAction() + { + Assert.Throws(() => new RelayCommand(null!)); + } +} diff --git a/SpdUp.Tests/ServiceTests.cs b/SpdUp.Tests/ServiceTests.cs new file mode 100644 index 0000000..53785a4 --- /dev/null +++ b/SpdUp.Tests/ServiceTests.cs @@ -0,0 +1,150 @@ +using SpdUp.Models; +using SpdUp.Services; + +namespace SpdUp.Tests; + +/// +/// Tests for services that can be tested without admin/system access. +/// +public class ServiceTests +{ + // ── NetworkService.PingAsync ────────────────────────────── + + [Fact] + public async Task PingAsync_Localhost_ReturnsPositive() + { + var ms = await NetworkService.PingAsync("127.0.0.1", 2000); + Assert.True(ms >= 0, $"Ping to localhost should succeed, got {ms}"); + } + + [Fact] + public async Task PingAsync_InvalidHost_ReturnsNegative() + { + var ms = await NetworkService.PingAsync("0.0.0.0", 500); + // 0.0.0.0 typically fails or returns -1 + // On some machines it may succeed — accept either + Assert.True(ms >= -1); + } + + // ── GameDetectionService ───────────────────────────────── + + [Fact] + public void GameDetectionService_InitialState_NoGameDetected() + { + var settings = new AppSettings(); + using var gds = new GameDetectionService(settings); + + Assert.Null(gds.CurrentGameProcess); + } + + [Fact] + public void GameDetectionService_AddGameProcess() + { + var settings = new AppSettings(); + using var gds = new GameDetectionService(settings); + + gds.AddGameProcess("TestGame"); + // AddGameProcess should not throw + } + + [Fact] + public void GameDetectionService_StartAndStop_DoesNotThrow() + { + var settings = new AppSettings(); + using var gds = new GameDetectionService(settings); + + gds.Start(60000); // very long interval so it doesn't actually poll during test + gds.Stop(); + } + + [Fact] + public void GameDetectionService_Dispose_DoesNotThrow() + { + var settings = new AppSettings(); + var gds = new GameDetectionService(settings); + gds.Dispose(); + // Double dispose should not throw + gds.Dispose(); + } + + // ── OptimizationService ────────────────────────────────── + + [Fact] + public void OptimizationService_InitialState() + { + var settings = new AppSettings(); + var svc = new OptimizationService(settings); + + Assert.False(svc.IsBoosted); + Assert.Null(svc.BoostedProcessName); + } + + [Fact] + public void OptimizationService_Events_CanSubscribe() + { + var settings = new AppSettings(); + var svc = new OptimizationService(settings); + + bool activated = false; + bool deactivated = false; + svc.BoostActivated += _ => activated = true; + svc.BoostDeactivated += () => deactivated = true; + + // Just verify event subscription doesn't throw + Assert.False(activated); + Assert.False(deactivated); + } + + // ── ServiceManagementService ───────────────────────────── + + [Fact] + public void KnownNonCriticalServices_ContainsExpectedEntries() + { + var known = ServiceManagementService.KnownNonCriticalServices; + + Assert.True(known.ContainsKey("SysMain")); + Assert.True(known.ContainsKey("WSearch")); + Assert.True(known.ContainsKey("DiagTrack")); + Assert.True(known.ContainsKey("XblAuthManager")); + Assert.True(known.Count >= 10, "Should have at least 10 known services"); + } + + [Fact] + public void GetServiceList_NonExistentService_ReturnsNotFound() + { + var entries = ServiceManagementService.GetServiceList(new[] { "SpdUpFakeService12345" }); + + Assert.Single(entries); + Assert.Equal("NotFound", entries[0].Status); + Assert.Equal("SpdUpFakeService12345", entries[0].ServiceName); + } + + [Fact] + public void GetServiceList_EmptyInput_ReturnsEmpty() + { + var entries = ServiceManagementService.GetServiceList(Array.Empty()); + Assert.Empty(entries); + } + + // ── ShaderCleanResult ──────────────────────────────────── + + [Fact] + public void ShaderCleanResult_MBFreed_LargeValue() + { + var r = new ShaderCleanResult(100, 1_073_741_824); // 1 GB + Assert.Equal(1024.0, r.MBFreed, 0.1); + } + + // ── RamOptimizationService ─────────────────────────────── + + [Fact] + public void RamOptimizationService_GetMemoryStatus_ReturnsValidValues() + { + var (totalMB, availMB, loadPercent) = RamOptimizationService.GetMemoryStatus(); + + Assert.True(totalMB > 0, "Total RAM should be > 0"); + Assert.True(availMB > 0, "Available RAM should be > 0"); + Assert.True(availMB <= totalMB, "Available should be <= total"); + Assert.InRange(loadPercent, (uint)1, (uint)100); + } +} diff --git a/SpdUp.Tests/SpdUp.Tests.csproj b/SpdUp.Tests/SpdUp.Tests.csproj new file mode 100644 index 0000000..d673825 --- /dev/null +++ b/SpdUp.Tests/SpdUp.Tests.csproj @@ -0,0 +1,30 @@ + + + + net8.0-windows + enable + enable + true + true + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/SpdUp.Tests/ViewModelTests.cs b/SpdUp.Tests/ViewModelTests.cs new file mode 100644 index 0000000..88e4e1d --- /dev/null +++ b/SpdUp.Tests/ViewModelTests.cs @@ -0,0 +1,157 @@ +using System.Collections.Specialized; +using SpdUp.Core; +using SpdUp.ViewModels; + +namespace SpdUp.Tests; + +/// +/// Tests for DebugLogViewModel. +/// Uses a helper to run on STA thread for WPF Dispatcher compatibility. +/// +public class DebugLogViewModelTests +{ + private static void RunOnSta(Action action) + { + Exception? caught = null; + var thread = new Thread(() => + { + try { action(); } + catch (Exception ex) { caught = ex; } + }); + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + thread.Join(); + if (caught is not null) + throw new Xunit.Sdk.XunitException($"STA test failed: {caught.Message}", caught); + } + + [Fact] + public void Initial_LogLines_IsEmpty() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + Assert.Empty(vm.LogLines); + }); + } + + [Fact] + public void AutoScroll_DefaultTrue() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + Assert.True(vm.AutoScroll); + }); + } + + [Fact] + public void AutoScroll_Settable() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + vm.AutoScroll = false; + Assert.False(vm.AutoScroll); + }); + } + + [Fact] + public void FilterText_DefaultEmpty() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + Assert.Equal(string.Empty, vm.FilterText); + }); + } + + [Fact] + public void FilterText_RaisesPropertyChanged() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + var changed = new List(); + vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName!); + + vm.FilterText = "error"; + + Assert.Contains("FilterText", changed); + Assert.Contains("FilteredLogLines", changed); + }); + } + + [Fact] + public void ClearCommand_ClearsLogLines() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + vm.LogLines.Add("test line 1"); + vm.LogLines.Add("test line 2"); + + vm.ClearCommand.Execute(null); + + Assert.Empty(vm.LogLines); + }); + } + + [Fact] + public void FilteredLogLines_NoFilter_ReturnsAll() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + vm.LogLines.Add("[INFO] started"); + vm.LogLines.Add("[ERROR] failed"); + vm.LogLines.Add("[WARN] low memory"); + + var filtered = vm.FilteredLogLines.ToList(); + Assert.Equal(3, filtered.Count); + }); + } + + [Fact] + public void FilteredLogLines_WithFilter_ReturnsMatches() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + vm.LogLines.Add("[INFO] started"); + vm.LogLines.Add("[ERROR] something failed"); + vm.LogLines.Add("[WARN] low memory"); + + vm.FilterText = "error"; + + var filtered = vm.FilteredLogLines.ToList(); + Assert.Single(filtered); + Assert.Contains("ERROR", filtered[0]); + }); + } + + [Fact] + public void FilteredLogLines_CaseInsensitive() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + vm.LogLines.Add("[INFO] Test Message"); + + vm.FilterText = "test message"; + + Assert.Single(vm.FilteredLogLines); + }); + } + + [Fact] + public void Commands_AreNotNull() + { + RunOnSta(() => + { + var vm = new DebugLogViewModel(); + Assert.NotNull(vm.ClearCommand); + Assert.NotNull(vm.CopyAllCommand); + }); + } +} diff --git a/SpdUp.csproj b/SpdUp.csproj new file mode 100644 index 0000000..57213f9 --- /dev/null +++ b/SpdUp.csproj @@ -0,0 +1,25 @@ + + + + WinExe + net8.0-windows + enable + enable + true + app.manifest + true + $(DefaultItemExcludes);SpdUp.Tests\** + + + + + + + + + + + + + + diff --git a/SpdUp.sln b/SpdUp.sln new file mode 100644 index 0000000..80ffc80 --- /dev/null +++ b/SpdUp.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpdUp", "SpdUp.csproj", "{F773DED5-00C7-8FFD-6136-0562AFD6C350}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpdUp.Tests", "SpdUp.Tests\SpdUp.Tests.csproj", "{E3683959-5FCC-49BA-ADAD-3A4F4225F93E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x64.ActiveCfg = Debug|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x64.Build.0 = Debug|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x86.ActiveCfg = Debug|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Debug|x86.Build.0 = Debug|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|Any CPU.Build.0 = Release|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x64.ActiveCfg = Release|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x64.Build.0 = Release|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x86.ActiveCfg = Release|Any CPU + {F773DED5-00C7-8FFD-6136-0562AFD6C350}.Release|x86.Build.0 = Release|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x64.ActiveCfg = Debug|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x64.Build.0 = Debug|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x86.ActiveCfg = Debug|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Debug|x86.Build.0 = Debug|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|Any CPU.Build.0 = Release|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x64.ActiveCfg = Release|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x64.Build.0 = Release|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x86.ActiveCfg = Release|Any CPU + {E3683959-5FCC-49BA-ADAD-3A4F4225F93E}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {50BCE545-BC95-4338-A07B-8D3FA391AB36} + EndGlobalSection +EndGlobal diff --git a/Themes/DarkTheme.xaml b/Themes/DarkTheme.xaml new file mode 100644 index 0000000..253b06a --- /dev/null +++ b/Themes/DarkTheme.xaml @@ -0,0 +1,254 @@ + + + + + + #0D0D0F + #16161A + #1E1E24 + #2A2A32 + #2E2E38 + #E8E8EC + #8B8B96 + #30D158 + #0A84FF + #FF453A + #FF9F0A + #BF5AF2 + #64D2FF + + + #55CDFC + #F7A8B8 + #FFFFFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ViewModels/BoostViewModel.cs b/ViewModels/BoostViewModel.cs new file mode 100644 index 0000000..c8422e6 --- /dev/null +++ b/ViewModels/BoostViewModel.cs @@ -0,0 +1,46 @@ +using System.Windows.Input; +using SpdUp.Core; +using SpdUp.Services; + +namespace SpdUp.ViewModels; + +/// +/// Boost page ViewModel. +/// +public sealed class BoostViewModel : ObservableObject +{ + private readonly MainViewModel _main; + private readonly OptimizationService _optimizationService; + + private string _boostLog = string.Empty; + public string BoostLog + { + get => _boostLog; + set => SetProperty(ref _boostLog, value); + } + + public MainViewModel Main => _main; + + public ICommand BoostCommand { get; } + public ICommand CleanRamCommand { get; } + public ICommand CleanShadersCommand { get; } + + public BoostViewModel(MainViewModel main, OptimizationService optimizationService) + { + _main = main; + _optimizationService = optimizationService; + + BoostCommand = _main.BoostCommand; + CleanRamCommand = _main.CleanRamCommand; + CleanShadersCommand = _main.CleanShaderCacheCommand; + + _optimizationService.BoostActivated += msg => + { + BoostLog = $"[{DateTime.Now:HH:mm:ss}] BOOST ON\n{msg}\n\n{BoostLog}"; + }; + _optimizationService.BoostDeactivated += () => + { + BoostLog = $"[{DateTime.Now:HH:mm:ss}] BOOST OFF – Restored.\n\n{BoostLog}"; + }; + } +} diff --git a/ViewModels/DashboardViewModel.cs b/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000..1ea7437 --- /dev/null +++ b/ViewModels/DashboardViewModel.cs @@ -0,0 +1,16 @@ +using SpdUp.Core; + +namespace SpdUp.ViewModels; + +/// +/// Dashboard page – just binds to MainViewModel's live metrics. +/// +public sealed class DashboardViewModel : ObservableObject +{ + public MainViewModel Main { get; } + + public DashboardViewModel(MainViewModel main) + { + Main = main; + } +} diff --git a/ViewModels/DebugLogViewModel.cs b/ViewModels/DebugLogViewModel.cs new file mode 100644 index 0000000..7b7bc01 --- /dev/null +++ b/ViewModels/DebugLogViewModel.cs @@ -0,0 +1,74 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using System.Windows.Threading; +using SpdUp.Core; + +namespace SpdUp.ViewModels; + +/// +/// Debug Log page – live scrolling log of every action the application performs. +/// +public sealed class DebugLogViewModel : ObservableObject +{ + private const int MaxLines = 2000; + private readonly Dispatcher _dispatcher; + + public ObservableCollection LogLines { get; } = new(); + + private string _filterText = string.Empty; + public string FilterText + { + get => _filterText; + set + { + if (SetProperty(ref _filterText, value)) + OnPropertyChanged(nameof(FilteredLogLines)); + } + } + + public IEnumerable FilteredLogLines => + string.IsNullOrWhiteSpace(_filterText) + ? LogLines + : LogLines.Where(l => l.Contains(_filterText, StringComparison.OrdinalIgnoreCase)); + + private bool _autoScroll = true; + public bool AutoScroll + { + get => _autoScroll; + set => SetProperty(ref _autoScroll, value); + } + + public ICommand ClearCommand { get; } + public ICommand CopyAllCommand { get; } + + public DebugLogViewModel() + { + _dispatcher = Dispatcher.CurrentDispatcher; + + ClearCommand = new RelayCommand(() => { LogLines.Clear(); OnPropertyChanged(nameof(FilteredLogLines)); }); + CopyAllCommand = new RelayCommand(() => + { + try + { + var text = string.Join(Environment.NewLine, LogLines); + System.Windows.Clipboard.SetText(text); + } + catch { } + }); + + // Subscribe to all log events + Logger.LogWritten += OnLogWritten; + } + + private void OnLogWritten(string line) + { + _dispatcher.BeginInvoke(() => + { + LogLines.Add(line); + while (LogLines.Count > MaxLines) + LogLines.RemoveAt(0); + + OnPropertyChanged(nameof(FilteredLogLines)); + }); + } +} diff --git a/ViewModels/GameLibraryViewModel.cs b/ViewModels/GameLibraryViewModel.cs new file mode 100644 index 0000000..bc3c66a --- /dev/null +++ b/ViewModels/GameLibraryViewModel.cs @@ -0,0 +1,72 @@ +using System.Collections.ObjectModel; +using System.Windows.Input; +using Microsoft.Win32; +using SpdUp.Core; +using SpdUp.Models; +using SpdUp.Services; + +namespace SpdUp.ViewModels; + +/// +/// Game Library page – manage detected and manually-added games. +/// +public sealed class GameLibraryViewModel : ObservableObject +{ + private readonly AppSettings _settings; + private readonly GameDetectionService _detectionService; + + public ObservableCollection Games { get; } + + private GameEntry? _selectedGame; + public GameEntry? SelectedGame + { + get => _selectedGame; + set => SetProperty(ref _selectedGame, value); + } + + public ICommand AddGameCommand { get; } + public ICommand RemoveGameCommand { get; } + + public GameLibraryViewModel(AppSettings settings, GameDetectionService detectionService) + { + _settings = settings; + _detectionService = detectionService; + Games = new ObservableCollection(_settings.Games); + + AddGameCommand = new RelayCommand(AddGame); + RemoveGameCommand = new RelayCommand(RemoveGame, () => SelectedGame is not null); + } + + private void AddGame() + { + var dlg = new OpenFileDialog + { + Filter = "Executables (*.exe)|*.exe", + Title = "Select Game Executable" + }; + + if (dlg.ShowDialog() == true) + { + var entry = new GameEntry + { + Name = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName), + ExecutablePath = dlg.FileName, + ProcessName = GameEntry.ProcessNameFromPath(dlg.FileName), + LauncherSource = "Manual" + }; + + Games.Add(entry); + _settings.Games.Add(entry); + _detectionService.AddGameProcess(entry.ProcessName); + _settings.Save(); + } + } + + private void RemoveGame() + { + if (SelectedGame is null) return; + _settings.Games.Remove(SelectedGame); + Games.Remove(SelectedGame); + _settings.Save(); + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..5d16e90 --- /dev/null +++ b/ViewModels/MainViewModel.cs @@ -0,0 +1,264 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Windows; +using System.Windows.Input; +using System.Windows.Threading; +using SpdUp.Core; +using SpdUp.Models; +using SpdUp.Services; + +namespace SpdUp.ViewModels; + +/// +/// Root ViewModel – drives the main window, navigation, boost, and monitoring. +/// +public sealed class MainViewModel : ObservableObject, IDisposable +{ + // ── Services ─────────────────────────────────────────────────────── + private readonly AppSettings _settings; + private readonly OptimizationService _optimizationService; + private readonly GameDetectionService _gameDetectionService; + private readonly MonitoringService _monitoringService; + private readonly NetworkService _networkService = new(); + private readonly Dispatcher _dispatcher; + + // ── Sub-ViewModels ───────────────────────────────────────────────── + public DashboardViewModel Dashboard { get; } + public BoostViewModel Boost { get; } + public MonitorViewModel Monitor { get; } + public GameLibraryViewModel GameLibrary { get; } + public ProcessManagerViewModel ProcessManager { get; } + public ServiceManagerViewModel ServiceManager { get; } + public SettingsViewModel Settings { get; } + public DebugLogViewModel DebugLog { get; } + + // ── Navigation ───────────────────────────────────────────────────── + private object _currentPage; + public object CurrentPage + { + get => _currentPage; + set => SetProperty(ref _currentPage, value); + } + + private string _currentPageTitle = "Dashboard"; + public string CurrentPageTitle + { + get => _currentPageTitle; + set => SetProperty(ref _currentPageTitle, value); + } + + // ── Status ───────────────────────────────────────────────────────── + private string _statusText = "Ready"; + public string StatusText + { + get => _statusText; + set => SetProperty(ref _statusText, value); + } + + private bool _isBoosted; + public bool IsBoosted + { + get => _isBoosted; + set => SetProperty(ref _isBoosted, value); + } + + // ── Live Metrics (bound by views & widget) ───────────────────────── + private float _cpuUsage; + public float CpuUsage { get => _cpuUsage; set => SetProperty(ref _cpuUsage, value); } + + private float _ramUsagePercent; + public float RamUsagePercent { get => _ramUsagePercent; set => SetProperty(ref _ramUsagePercent, value); } + + private float _ramUsedMB; + public float RamUsedMB { get => _ramUsedMB; set => SetProperty(ref _ramUsedMB, value); } + + private float _ramTotalMB; + public float RamTotalMB { get => _ramTotalMB; set => SetProperty(ref _ramTotalMB, value); } + + private float _gpuUsage; + public float GpuUsage { get => _gpuUsage; set => SetProperty(ref _gpuUsage, value); } + + private float _cpuTemp; + public float CpuTemp { get => _cpuTemp; set => SetProperty(ref _cpuTemp, value); } + + private float _gpuTemp; + public float GpuTemp { get => _gpuTemp; set => SetProperty(ref _gpuTemp, value); } + + private float _pingMs; + public float PingMs { get => _pingMs; set => SetProperty(ref _pingMs, value); } + + // ── Commands ─────────────────────────────────────────────────────── + public ICommand NavigateCommand { get; } + public ICommand BoostCommand { get; } + public ICommand ToggleWidgetCommand { get; } + public ICommand CleanRamCommand { get; } + public ICommand CleanShaderCacheCommand { get; } + + // ── Widget ───────────────────────────────────────────────────────── + private bool _isWidgetVisible; + public bool IsWidgetVisible + { + get => _isWidgetVisible; + set + { + if (SetProperty(ref _isWidgetVisible, value)) + _settings.ShowWidget = value; + } + } + + public MainViewModel() + { + _dispatcher = Dispatcher.CurrentDispatcher; + _settings = AppSettings.Load(); + + // Initialise services + _optimizationService = new OptimizationService(_settings); + _gameDetectionService = new GameDetectionService(_settings); + _monitoringService = new MonitoringService(_settings.PingTarget); + + // Sub-ViewModels + Dashboard = new DashboardViewModel(this); + Boost = new BoostViewModel(this, _optimizationService); + Monitor = new MonitorViewModel(this); + GameLibrary = new GameLibraryViewModel(_settings, _gameDetectionService); + ProcessManager = new ProcessManagerViewModel(); + ServiceManager = new ServiceManagerViewModel(_settings); + Settings = new SettingsViewModel(_settings, this); + DebugLog = new DebugLogViewModel(); + + _currentPage = Dashboard; + + // Commands + NavigateCommand = new RelayCommand(Navigate); + BoostCommand = new RelayCommand(async () => await ToggleBoostAsync()); + ToggleWidgetCommand = new RelayCommand(() => IsWidgetVisible = !IsWidgetVisible); + CleanRamCommand = new RelayCommand(async () => await CleanRamAsync()); + CleanShaderCacheCommand = new RelayCommand(async () => await CleanShaderCacheAsync()); + + // Wire up monitoring + _monitoringService.MetricsUpdated += OnMetricsUpdated; + _monitoringService.Start(_settings.MonitoringIntervalMs); + + // Wire up game detection + _gameDetectionService.GameStarted += OnGameStarted; + _gameDetectionService.GameStopped += OnGameStopped; + if (_settings.AutoDetectGames) + _gameDetectionService.Start(); + + // Widget state + _isWidgetVisible = _settings.ShowWidget; + + Logger.PurgeOldLogs(); + Logger.Info("Application started."); + } + + // ── Navigation ───────────────────────────────────────────────────── + private void Navigate(string? pageName) + { + (CurrentPage, CurrentPageTitle) = pageName switch + { + "Dashboard" => ((object)Dashboard, "Dashboard"), + "Boost" => (Boost, "Game Boost"), + "Monitor" => (Monitor, "System Monitor"), + "Games" => (GameLibrary, "Game Library"), + "Processes" => (ProcessManager, "Process Manager"), + "Services" => (ServiceManager, "Service Manager"), + "Settings" => (Settings, "Settings"), + "Debug" => (DebugLog, "Debug Log"), + _ => (Dashboard, "Dashboard") + }; + } + + // ── Boost Toggle ─────────────────────────────────────────────────── + private async Task ToggleBoostAsync() + { + if (_optimizationService.IsBoosted) + { + StatusText = "Deactivating boost..."; + await _optimizationService.DeactivateBoostAsync(); + IsBoosted = false; + StatusText = "Boost deactivated – settings restored."; + } + else + { + StatusText = "Activating boost..."; + var gameProcess = _gameDetectionService.CurrentGameProcess; + var result = await _optimizationService.ActivateBoostAsync(gameProcess); + IsBoosted = result.Success; + StatusText = result.Success ? "🚀 Boost Active" : result.Message; + } + } + + // ── Quick Actions ────────────────────────────────────────────────── + private async Task CleanRamAsync() + { + StatusText = "Cleaning RAM..."; + var result = await Task.Run(() => new RamOptimizationService().FullCleanup()); + StatusText = $"RAM cleaned: {result.TrimmedCount} processes trimmed."; + } + + private async Task CleanShaderCacheAsync() + { + StatusText = "Cleaning shader caches..."; + var result = await Task.Run(() => ShaderCacheCleanerService.CleanAll()); + StatusText = $"Shader cache: {result.FilesDeleted} files, {result.MBFreed:F1} MB freed."; + } + + // ── Monitoring Callback ──────────────────────────────────────────── + private void OnMetricsUpdated(SystemMetrics m) + { + _dispatcher.BeginInvoke(() => + { + CpuUsage = m.CpuUsage; + RamUsagePercent = m.RamUsagePercent; + RamUsedMB = m.RamUsageMB; + RamTotalMB = m.RamTotalMB; + GpuUsage = m.GpuUsage; + CpuTemp = m.CpuTemperature; + GpuTemp = m.GpuTemperature; + PingMs = m.NetworkPingMs; + }); + } + + // ── Game Detection Callbacks ─────────────────────────────────────── +#pragma warning disable CS4014 // Fire-and-forget is intentional for event handlers + private async void OnGameStarted(string processName) + { + _dispatcher.BeginInvoke(() => StatusText = $"Game detected: {processName}"); + + if (_settings.AutoBoostOnGameLaunch && !_optimizationService.IsBoosted) + { + var result = await _optimizationService.ActivateBoostAsync(processName); + _dispatcher.BeginInvoke(() => + { + IsBoosted = result.Success; + StatusText = result.Success ? $"🚀 Auto-Boost for {processName}" : result.Message; + }); + } + } + + private async void OnGameStopped(string processName) + { + if (_optimizationService.IsBoosted) + { + await _optimizationService.DeactivateBoostAsync(); + _dispatcher.BeginInvoke(() => + { + IsBoosted = false; + StatusText = $"Game exited: {processName} – boost deactivated."; + }); + } + } +#pragma warning restore CS4014 + + public void SaveSettings() => _settings.Save(); + + public void Dispose() + { + _monitoringService.MetricsUpdated -= OnMetricsUpdated; + _monitoringService.Dispose(); + _gameDetectionService.Dispose(); + _settings.Save(); + Logger.Info("Application closed."); + } +} diff --git a/ViewModels/MonitorViewModel.cs b/ViewModels/MonitorViewModel.cs new file mode 100644 index 0000000..afab8b4 --- /dev/null +++ b/ViewModels/MonitorViewModel.cs @@ -0,0 +1,16 @@ +using SpdUp.Core; + +namespace SpdUp.ViewModels; + +/// +/// System Monitor page – just binds to MainViewModel's live metrics. +/// +public sealed class MonitorViewModel : ObservableObject +{ + public MainViewModel Main { get; } + + public MonitorViewModel(MainViewModel main) + { + Main = main; + } +} diff --git a/ViewModels/ProcessManagerViewModel.cs b/ViewModels/ProcessManagerViewModel.cs new file mode 100644 index 0000000..f821ce6 --- /dev/null +++ b/ViewModels/ProcessManagerViewModel.cs @@ -0,0 +1,100 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Windows.Input; +using System.Windows.Threading; +using SpdUp.Core; +using SpdUp.Models; +using SpdUp.Services; + +namespace SpdUp.ViewModels; + +/// +/// Process Manager ViewModel. +/// +public sealed class ProcessManagerViewModel : ObservableObject +{ + private readonly DispatcherTimer _refreshTimer; + + public ObservableCollection Processes { get; } = new(); + + private ProcessEntry? _selectedProcess; + public ProcessEntry? SelectedProcess + { + get => _selectedProcess; + set => SetProperty(ref _selectedProcess, value); + } + + public ICommand RefreshCommand { get; } + public ICommand KillProcessCommand { get; } + public ICommand SetHighPriorityCommand { get; } + public ICommand SetNormalPriorityCommand { get; } + + public ProcessManagerViewModel() + { + RefreshCommand = new RelayCommand(RefreshProcesses); + KillProcessCommand = new RelayCommand(KillSelected, () => SelectedProcess is not null); + SetHighPriorityCommand = new RelayCommand(() => SetPriority(ProcessPriorityClass.High), () => SelectedProcess is not null); + SetNormalPriorityCommand = new RelayCommand(() => SetPriority(ProcessPriorityClass.Normal), () => SelectedProcess is not null); + + _refreshTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) }; + _refreshTimer.Tick += (_, _) => RefreshProcesses(); + + RefreshProcesses(); + _refreshTimer.Start(); + } + + private void RefreshProcesses() + { + var procs = Process.GetProcesses() + .Select(p => + { + try + { + return new ProcessEntry + { + Pid = p.Id, + Name = p.ProcessName, + WindowTitle = string.IsNullOrWhiteSpace(p.MainWindowTitle) ? null : p.MainWindowTitle, + MemoryMB = p.WorkingSet64 / 1048576f, + PriorityClass = TryGetPriority(p), + IsSystem = p.Id <= 4 + }; + } + catch { return null; } + finally { p.Dispose(); } + }) + .Where(e => e is not null) + .OrderByDescending(e => e!.MemoryMB) + .Take(200) + .ToList(); + + Processes.Clear(); + foreach (var p in procs) + Processes.Add(p!); + } + + private static string TryGetPriority(Process p) + { + try { return p.PriorityClass.ToString(); } + catch { return "N/A"; } + } + + private void KillSelected() + { + if (SelectedProcess is null) return; + OptimizationService.KillProcess(SelectedProcess.Pid); + RefreshProcesses(); + } + + private void SetPriority(ProcessPriorityClass priority) + { + if (SelectedProcess is null) return; + try + { + using var proc = Process.GetProcessById(SelectedProcess.Pid); + proc.PriorityClass = priority; + } + catch { } + RefreshProcesses(); + } +} diff --git a/ViewModels/ServiceManagerViewModel.cs b/ViewModels/ServiceManagerViewModel.cs new file mode 100644 index 0000000..32ecee8 --- /dev/null +++ b/ViewModels/ServiceManagerViewModel.cs @@ -0,0 +1,67 @@ +using System.Collections.ObjectModel; +using System.ServiceProcess; +using System.Windows.Input; +using SpdUp.Core; +using SpdUp.Models; +using SpdUp.Services; + +namespace SpdUp.ViewModels; + +/// +/// Service Manager ViewModel. +/// +public sealed class ServiceManagerViewModel : ObservableObject +{ + private readonly AppSettings _settings; + + public ObservableCollection Services { get; } = new(); + + private ServiceEntry? _selectedService; + public ServiceEntry? SelectedService + { + get => _selectedService; + set => SetProperty(ref _selectedService, value); + } + + public ICommand RefreshCommand { get; } + public ICommand StopServiceCommand { get; } + public ICommand StartServiceCommand { get; } + + public ServiceManagerViewModel(AppSettings settings) + { + _settings = settings; + + RefreshCommand = new RelayCommand(Refresh); + StopServiceCommand = new RelayCommand(StopSelected, () => SelectedService is not null); + StartServiceCommand = new RelayCommand(StartSelected, () => SelectedService is not null); + + Refresh(); + } + + private void Refresh() + { + Services.Clear(); + + // Show the configured services plus all known non-critical ones + var names = new HashSet(_settings.ServicesToStop, StringComparer.OrdinalIgnoreCase); + foreach (var k in ServiceManagementService.KnownNonCriticalServices.Keys) + names.Add(k); + + foreach (var entry in ServiceManagementService.GetServiceList(names)) + Services.Add(entry); + } + + private void StopSelected() + { + if (SelectedService is null) return; + ServiceManagementService.StopService(SelectedService.ServiceName); + Refresh(); + } + + private void StartSelected() + { + if (SelectedService is null) return; + ServiceManagementService.StartService(SelectedService.ServiceName); + Refresh(); + } +} diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..da05160 --- /dev/null +++ b/ViewModels/SettingsViewModel.cs @@ -0,0 +1,91 @@ +using System.Windows.Input; +using SpdUp.Core; +using SpdUp.Models; + +namespace SpdUp.ViewModels; + +/// +/// Settings page ViewModel. +/// +public sealed class SettingsViewModel : ObservableObject +{ + private readonly AppSettings _settings; + private readonly MainViewModel _main; + + public bool AutoDetectGames + { + get => _settings.AutoDetectGames; + set { _settings.AutoDetectGames = value; OnPropertyChanged(); Save(); } + } + + public bool AutoBoostOnGameLaunch + { + get => _settings.AutoBoostOnGameLaunch; + set { _settings.AutoBoostOnGameLaunch = value; OnPropertyChanged(); Save(); } + } + + public bool MinimizeToTray + { + get => _settings.MinimizeToTray; + set { _settings.MinimizeToTray = value; OnPropertyChanged(); Save(); } + } + + public bool StartWithWindows + { + get => _settings.StartWithWindows; + set { _settings.StartWithWindows = value; OnPropertyChanged(); Save(); } + } + + public bool WidgetCompactMode + { + get => _settings.WidgetCompactMode; + set { _settings.WidgetCompactMode = value; OnPropertyChanged(); Save(); } + } + + public double WidgetOpacity + { + get => _settings.WidgetOpacity; + set { _settings.WidgetOpacity = value; OnPropertyChanged(); Save(); } + } + + public int WidgetUpdateInterval + { + get => _settings.WidgetUpdateIntervalMs; + set { _settings.WidgetUpdateIntervalMs = value; OnPropertyChanged(); Save(); } + } + + public bool WidgetClickThrough + { + get => _settings.WidgetClickThrough; + set { _settings.WidgetClickThrough = value; OnPropertyChanged(); Save(); } + } + + public int MonitoringInterval + { + get => _settings.MonitoringIntervalMs; + set { _settings.MonitoringIntervalMs = value; OnPropertyChanged(); Save(); } + } + + public string PingTarget + { + get => _settings.PingTarget; + set { _settings.PingTarget = value; OnPropertyChanged(); Save(); } + } + + public string PreferredDns + { + get => _settings.PreferredDns; + set { _settings.PreferredDns = value; OnPropertyChanged(); Save(); } + } + + public ICommand SaveCommand { get; } + + public SettingsViewModel(AppSettings settings, MainViewModel main) + { + _settings = settings; + _main = main; + SaveCommand = new RelayCommand(Save); + } + + private void Save() => _settings.Save(); +} diff --git a/Views/BoostView.xaml b/Views/BoostView.xaml new file mode 100644 index 0000000..6e116d5 --- /dev/null +++ b/Views/BoostView.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/BoostView.xaml.cs b/Views/BoostView.xaml.cs new file mode 100644 index 0000000..98a3825 --- /dev/null +++ b/Views/BoostView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class BoostView : UserControl +{ + public BoostView() => InitializeComponent(); +} diff --git a/Views/DashboardView.xaml b/Views/DashboardView.xaml new file mode 100644 index 0000000..4b268f7 --- /dev/null +++ b/Views/DashboardView.xaml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/DashboardView.xaml.cs b/Views/DashboardView.xaml.cs new file mode 100644 index 0000000..7ba54cd --- /dev/null +++ b/Views/DashboardView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class DashboardView : UserControl +{ + public DashboardView() => InitializeComponent(); +} diff --git a/Views/DebugLogView.xaml b/Views/DebugLogView.xaml new file mode 100644 index 0000000..5e268e4 --- /dev/null +++ b/Views/DebugLogView.xaml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/DebugLogView.xaml.cs b/Views/DebugLogView.xaml.cs new file mode 100644 index 0000000..6a18fab --- /dev/null +++ b/Views/DebugLogView.xaml.cs @@ -0,0 +1,38 @@ +using System.Collections.Specialized; +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class DebugLogView : UserControl +{ + public DebugLogView() + { + InitializeComponent(); + + // Auto-scroll to bottom when new items are added + if (LogList.ItemsSource is INotifyCollectionChanged ncc) + { + ncc.CollectionChanged += (_, _) => + { + if (DataContext is ViewModels.DebugLogViewModel vm && vm.AutoScroll && LogList.Items.Count > 0) + { + LogList.ScrollIntoView(LogList.Items[^1]); + } + }; + } + + Loaded += (_, _) => + { + if (LogList.ItemsSource is INotifyCollectionChanged ncc2) + { + ncc2.CollectionChanged += (_, _) => + { + if (DataContext is ViewModels.DebugLogViewModel vm && vm.AutoScroll && LogList.Items.Count > 0) + { + LogList.ScrollIntoView(LogList.Items[^1]); + } + }; + } + }; + } +} diff --git a/Views/GameLibraryView.xaml b/Views/GameLibraryView.xaml new file mode 100644 index 0000000..15bffcd --- /dev/null +++ b/Views/GameLibraryView.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Views/GameLibraryView.xaml.cs b/Views/GameLibraryView.xaml.cs new file mode 100644 index 0000000..8485ad6 --- /dev/null +++ b/Views/GameLibraryView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class GameLibraryView : UserControl +{ + public GameLibraryView() => InitializeComponent(); +} diff --git a/Views/MonitorView.xaml b/Views/MonitorView.xaml new file mode 100644 index 0000000..d4b1f64 --- /dev/null +++ b/Views/MonitorView.xaml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/MonitorView.xaml.cs b/Views/MonitorView.xaml.cs new file mode 100644 index 0000000..8cd0d93 --- /dev/null +++ b/Views/MonitorView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class MonitorView : UserControl +{ + public MonitorView() => InitializeComponent(); +} diff --git a/Views/ProcessManagerView.xaml b/Views/ProcessManagerView.xaml new file mode 100644 index 0000000..653ebed --- /dev/null +++ b/Views/ProcessManagerView.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/ProcessManagerView.xaml.cs b/Views/ProcessManagerView.xaml.cs new file mode 100644 index 0000000..46f9c79 --- /dev/null +++ b/Views/ProcessManagerView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class ProcessManagerView : UserControl +{ + public ProcessManagerView() => InitializeComponent(); +} diff --git a/Views/ServiceManagerView.xaml b/Views/ServiceManagerView.xaml new file mode 100644 index 0000000..f1c15d0 --- /dev/null +++ b/Views/ServiceManagerView.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/ServiceManagerView.xaml.cs b/Views/ServiceManagerView.xaml.cs new file mode 100644 index 0000000..b5ca2b1 --- /dev/null +++ b/Views/ServiceManagerView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class ServiceManagerView : UserControl +{ + public ServiceManagerView() => InitializeComponent(); +} diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml new file mode 100644 index 0000000..1eba1a9 --- /dev/null +++ b/Views/SettingsView.xaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/SettingsView.xaml.cs b/Views/SettingsView.xaml.cs new file mode 100644 index 0000000..c3abe58 --- /dev/null +++ b/Views/SettingsView.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows.Controls; + +namespace SpdUp.Views; + +public partial class SettingsView : UserControl +{ + public SettingsView() => InitializeComponent(); +} diff --git a/Views/WidgetWindow.xaml b/Views/WidgetWindow.xaml new file mode 100644 index 0000000..f137905 --- /dev/null +++ b/Views/WidgetWindow.xaml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Views/WidgetWindow.xaml.cs b/Views/WidgetWindow.xaml.cs new file mode 100644 index 0000000..b21d8f1 --- /dev/null +++ b/Views/WidgetWindow.xaml.cs @@ -0,0 +1,83 @@ +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; +using SpdUp.ViewModels; + +namespace SpdUp.Views; + +/// +/// Floating desktop widget – borderless, transparent, always-on-top. +/// +public partial class WidgetWindow : Window +{ + private bool _expanded; + + // ── Win32 click-through support ─────────────────────────────────── + private const int GWL_EXSTYLE = -20; + private const int WS_EX_TRANSPARENT = 0x00000020; + private const int WS_EX_TOOLWINDOW = 0x00000080; + + [DllImport("user32.dll")] + private static extern int GetWindowLong(IntPtr hwnd, int index); + + [DllImport("user32.dll")] + private static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle); + + public WidgetWindow(MainViewModel vm) + { + InitializeComponent(); + DataContext = vm; + + // Restore position from settings + Left = vm.Settings.WidgetOpacity > 0 ? 100 : 100; // default + Top = 100; + + Loaded += (_, _) => + { + // Make it a tool window (no taskbar entry) + var hwnd = new WindowInteropHelper(this).Handle; + var exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TOOLWINDOW); + + Opacity = vm.Settings.WidgetOpacity; + + if (!vm.Settings.WidgetCompactMode) + ToggleExpand(); + }; + } + + private void Widget_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed) + DragMove(); + } + + private void Widget_MouseRightButtonDown(object sender, MouseButtonEventArgs e) + { + ToggleExpand(); + } + + private void Toggle_Click(object sender, RoutedEventArgs e) => ToggleExpand(); + private void Close_Click(object sender, RoutedEventArgs e) => Close(); + + private void ToggleExpand() + { + _expanded = !_expanded; + ExpandedPanel.Visibility = _expanded ? Visibility.Visible : Visibility.Collapsed; + } + + /// + /// Enable click-through: all mouse events pass through to windows below. + /// + public void SetClickThrough(bool enable) + { + var hwnd = new WindowInteropHelper(this).Handle; + var exStyle = GetWindowLong(hwnd, GWL_EXSTYLE); + + if (enable) + SetWindowLong(hwnd, GWL_EXSTYLE, exStyle | WS_EX_TRANSPARENT); + else + SetWindowLong(hwnd, GWL_EXSTYLE, exStyle & ~WS_EX_TRANSPARENT); + } +} diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..c14dafb --- /dev/null +++ b/app.manifest @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/build-installer.ps1 b/build-installer.ps1 new file mode 100644 index 0000000..4329ab6 --- /dev/null +++ b/build-installer.ps1 @@ -0,0 +1,86 @@ +<# +.SYNOPSIS + Build & package SpdUp – Gaming Booster +.DESCRIPTION + 1. Publishes the app as self-contained (win-x64) + 2. Optionally compiles the Inno Setup installer into a single .exe +.EXAMPLE + .\build-installer.ps1 # Publish only + .\build-installer.ps1 -Installer # Publish + compile installer +#> +param( + [switch]$Installer, + [string]$Configuration = "Release" +) + +$ErrorActionPreference = "Stop" +$root = $PSScriptRoot +$proj = Join-Path $root "SpdUp.csproj" +$pubDir = Join-Path $root "publish\SpdUp" +$issFile = Join-Path $root "Installer\SpdUpSetup.iss" + +# ── Step 1: Clean previous publish ──────────────────────── +Write-Host "`n=== Cleaning previous publish ===" -ForegroundColor Cyan +if (Test-Path $pubDir) { + Remove-Item $pubDir -Recurse -Force +} + +# ── Step 2: Publish self-contained ──────────────────────── +Write-Host "`n=== Publishing SpdUp (self-contained, win-x64, $Configuration) ===" -ForegroundColor Cyan +dotnet publish $proj ` + -c $Configuration ` + -r win-x64 ` + --self-contained true ` + -o $pubDir + +if ($LASTEXITCODE -ne 0) { + Write-Error "dotnet publish failed with exit code $LASTEXITCODE" + exit 1 +} + +$files = Get-ChildItem $pubDir -Recurse -File +$totalMB = [math]::Round(($files | Measure-Object -Property Length -Sum).Sum / 1MB, 1) +Write-Host "`nPublished $($files.Count) files ($totalMB MB) to: $pubDir" -ForegroundColor Green + +# ── Step 3 (optional): Compile Inno Setup installer ────── +if ($Installer) { + Write-Host "`n=== Compiling Inno Setup installer ===" -ForegroundColor Cyan + + # Try to find ISCC.exe (Inno Setup Compiler) + $isccPaths = @( + "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe", + "${env:ProgramFiles}\Inno Setup 6\ISCC.exe", + "${env:ProgramFiles(x86)}\Inno Setup 5\ISCC.exe" + ) + $iscc = $isccPaths | Where-Object { Test-Path $_ } | Select-Object -First 1 + + if (-not $iscc) { + Write-Host "" + Write-Warning "Inno Setup not found! Install it from: https://jrsoftware.org/isdl.php" + Write-Host "After installing, run this script again with -Installer" -ForegroundColor Yellow + Write-Host "`nAlternatively, open '$issFile' directly in Inno Setup and click Compile." -ForegroundColor Yellow + exit 1 + } + + Write-Host "Using: $iscc" + & $iscc $issFile + + if ($LASTEXITCODE -ne 0) { + Write-Error "Inno Setup compilation failed with exit code $LASTEXITCODE" + exit 1 + } + + $setupExe = Get-ChildItem (Join-Path $root "publish") -Filter "SpdUp_Setup_*.exe" | Select-Object -First 1 + if ($setupExe) { + $sizeMB = [math]::Round($setupExe.Length / 1MB, 1) + Write-Host "`nInstaller created: $($setupExe.FullName) ($sizeMB MB)" -ForegroundColor Green + } +} +else { + Write-Host "`n=== Skipping installer (use -Installer to compile setup.exe) ===" -ForegroundColor Yellow + Write-Host "To create the installer, either:" + Write-Host " 1. Run: .\build-installer.ps1 -Installer" + Write-Host " 2. Install Inno Setup 6, open '$issFile', click Compile" +} + +Write-Host "`nDone!" -ForegroundColor Green diff --git a/website/index.html b/website/index.html new file mode 100644 index 0000000..1d01065 --- /dev/null +++ b/website/index.html @@ -0,0 +1,960 @@ + + + + + + SpdUp – Gaming Booster for Windows + + + + + + + + + + + + ⚡ SpdUp + + Features + Preview + How it Works + Tech + Download + + ☰ + + + + + + + Free & Open Source + + Boost YourGaming Performance + + + SpdUp optimizes your Windows PC for gaming — CPU priority, RAM cleanup, GPU tweaks, network tuning, and service management. All in one click. + + + ⬇ Download for Windows + Learn More → + + + + 8+ + Optimization Steps + + + 0ms + Timer Resolution + + + 1‑Click + Boost Activation + + + 100% + Free Forever + + + + + + + + + + + ⚡ SpdUp – Gaming Booster + + — + ☐ + ✕ + + + + + ⚡ SpdUp Gaming Booster + Main + 📊 Dashboard + 🚀 Game Boost + 📈 System Monitor + Manage + 🎮 Game Library + 📋 Processes + ⚙ Services + Other + 🖥 Widget + 🐛 Debug Log + ⚙ Settings + + + + + + + + Made with ❤️ by Mandy + + + + + Dashboard + + CPU 12% + RAM 54% + GPU 3% + Ping 14ms + + + + + CPU Usage + 12% + + + RAM Usage + 8.7 GB + + + GPU Temp + 42°C + + + Network + 14ms + + + + 🚀 + + Game Boost Ready + Click to optimize CPU, RAM, GPU, network, and services for peak gaming performance. + + CPU Priority + RAM Cleanup + Timer Res + GPU Tweaks + Network + Services + + + + + + + + + + + + + Features + + Everything You Need to Win + + + SpdUp packs powerful optimization tools into a beautiful dark UI — designed for gamers who want every last frame. + + + + 🚀 + One-Click Boost + Activate Game Boost to set CPU priority, flush RAM, lower timer resolution, tweak GPU power profiles, optimize network settings, and stop unnecessary services — all with a single click. + + + 🧹 + RAM Optimizer + Deep RAM cleanup that goes beyond simple cache clearing: trims working sets of all processes and purges the Windows Standby List using kernel-level calls for maximum free memory. + + + 📈 + Real-Time Monitor + Live system metrics including CPU, GPU, RAM utilization, temperatures, fan speeds, and network latency — powered by LibreHardwareMonitor for accurate hardware readings. + + + 🌐 + Network Optimizer + Applies TCP registry tweaks (Nagle's algorithm, receive window auto-tuning) and monitors ping latency in real-time. Designed for low-latency competitive gaming. + + + ⚙️ + Service Manager + Smart service detection identifies non-essential Windows services that consume resources. Safely disable them during gaming and restore them afterward. + + + 🎮 + Game Detection + Automatically detects when you launch a game and applies boost optimizations. Detects 30+ popular games including Steam, Epic, Riot, Blizzard, and EA titles. + + + 🗑️ + Shader Cache Cleaner + Clears DirectX, NVIDIA, AMD, and Intel shader caches to free disk space and fix rendering issues. Scans all known cache locations automatically. + + + 📋 + Process Manager + View and manage running processes with the ability to set CPU priority and affinity. Kill resource-hungry background processes to reclaim performance. + + + 🖥️ + Desktop Widget + Always-on-top floating widget shows CPU, RAM, GPU, and ping at a glance while you game — with a one-click boost button right on your desktop. + + + + + + + + + Preview + + See It In Action + + + A dark, modern UI designed for gamers who appreciate clean aesthetics. + + + Dashboard + Game Boost + Monitor + Processes + Services + Debug Log + + + + + 📊 + Dashboard View + Place your screenshot in website/screenshots/dashboard.png + + + + + + + + + How it Works + + Three Steps to Peak Performance + + + + 1 + Install & Launch + Run the installer and launch SpdUp. It automatically requests administrator privileges for deep system optimizations. + + + 2 + Hit Boost + Click the Boost button — SpdUp instantly optimizes CPU, RAM, GPU, network, timer resolution, and disables unnecessary services. + + + 3 + Game On + Play with maximum performance. When you're done, click Restore to bring everything back to normal. That's it. + + + + + + + + + Under the Hood + + Built with Modern Technology + + + + 🏗️ + + .NET 8 + WPF + Built on the latest .NET runtime. Self-contained — no framework install needed. + + + + 🧠 + + MVVM Architecture + Clean separation of concerns with full data binding and reactive UI updates. + + + + 🔧 + + Kernel-Level Tuning + P/Invoke calls to NtSetTimerResolution, EmptyWorkingSet, and standby list purge. + + + + 🌡️ + + Hardware Monitoring + LibreHardwareMonitor integration for real CPU/GPU temps, fan speeds, and clocks. + + + + 🛡️ + + Admin Privileges + Runs elevated with proper manifest for safe access to protected system APIs. + + + + 🎨 + + Custom Dark Theme + Full custom chrome, styled scrollbars, buttons, and data grids. No Windows chrome. + + + + 📦 + + Single Installer + Inno Setup packages everything into one .exe — install, run, done. + + + + 📝 + + Debug Logging + Built-in debug log with live filtering, copy, and file logging for troubleshooting. + + + + + + + + + + System Requirements + + + 💻 + + Windows 10/11 + 64-bit, version 1809 or later + + + + 🧮 + + 4 GB RAM + 8 GB or more recommended + + + + 💾 + + 200 MB Disk Space + For the installed application + + + + + + + + + + + Ready to Boost? + Download SpdUp for free and unlock your PC's full gaming potential. + + ⬇ Download SpdUp v1.0.0 + + + 📦 ~60 MB + 🪟 Windows 10/11 + 🔓 No account needed + 💚 Free forever + + + + + + + + + + + +
+ + + + + +
+ A free, open-source Windows gaming optimizer that boosts your PC's performance with a single click. +
+ Dark gaming UI with custom window chrome, sidebar navigation, and live system metrics +
+ +
+ SpdUp optimizes your Windows PC for gaming — CPU priority, RAM cleanup, GPU tweaks, network tuning, and service management. All in one click. +
+ SpdUp packs powerful optimization tools into a beautiful dark UI — designed for gamers who want every last frame. +
Activate Game Boost to set CPU priority, flush RAM, lower timer resolution, tweak GPU power profiles, optimize network settings, and stop unnecessary services — all with a single click.
Deep RAM cleanup that goes beyond simple cache clearing: trims working sets of all processes and purges the Windows Standby List using kernel-level calls for maximum free memory.
Live system metrics including CPU, GPU, RAM utilization, temperatures, fan speeds, and network latency — powered by LibreHardwareMonitor for accurate hardware readings.
Applies TCP registry tweaks (Nagle's algorithm, receive window auto-tuning) and monitors ping latency in real-time. Designed for low-latency competitive gaming.
Smart service detection identifies non-essential Windows services that consume resources. Safely disable them during gaming and restore them afterward.
Automatically detects when you launch a game and applies boost optimizations. Detects 30+ popular games including Steam, Epic, Riot, Blizzard, and EA titles.
Clears DirectX, NVIDIA, AMD, and Intel shader caches to free disk space and fix rendering issues. Scans all known cache locations automatically.
View and manage running processes with the ability to set CPU priority and affinity. Kill resource-hungry background processes to reclaim performance.
Always-on-top floating widget shows CPU, RAM, GPU, and ping at a glance while you game — with a one-click boost button right on your desktop.
+ A dark, modern UI designed for gamers who appreciate clean aesthetics. +
Run the installer and launch SpdUp. It automatically requests administrator privileges for deep system optimizations.
Click the Boost button — SpdUp instantly optimizes CPU, RAM, GPU, network, timer resolution, and disables unnecessary services.
Play with maximum performance. When you're done, click Restore to bring everything back to normal. That's it.
Built on the latest .NET runtime. Self-contained — no framework install needed.
Clean separation of concerns with full data binding and reactive UI updates.
P/Invoke calls to NtSetTimerResolution, EmptyWorkingSet, and standby list purge.
LibreHardwareMonitor integration for real CPU/GPU temps, fan speeds, and clocks.
Runs elevated with proper manifest for safe access to protected system APIs.
Full custom chrome, styled scrollbars, buttons, and data grids. No Windows chrome.
Inno Setup packages everything into one .exe — install, run, done.
Built-in debug log with live filtering, copy, and file logging for troubleshooting.
64-bit, version 1809 or later
8 GB or more recommended
For the installed application
Download SpdUp for free and unlock your PC's full gaming potential.