Initial commit: SpdUp Windows Gaming Booster v1.0.0

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