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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ Versions before 1.0.0 are pre-release: features and defaults may still change.

## [Unreleased]

## [0.1.69] - 2026-08-08

### Fixed
- **The AMD 3D V-Cache Optimizer is now detected properly.** 0.1.68 still reported
it as missing on machines that had it. AMD installs it as two separate pieces — a
kernel driver and a helper service — and the app was only ever looking at the
second one, so a PC carrying the first looked like it had nothing installed. It
now finds either, and tells you which one it found.
- **The panel no longer claims the optimizer "isn't installed" when it simply
couldn't check.** Being unable to read and having read and found nothing are now
reported as different things.
- **Clearer advice when the optimizer really is absent.** The panel names the
download it comes in ("AMD Chipset Software", listed inside it as "AMD 3D V-Cache
Performance Optimizer Driver") instead of saying "chipset driver", and makes the
important point up front: on the recommended CPPC = Cache setting the optimizer
is not needed at all, so its absence is not a problem to fix. It only matters if
you choose CPPC = Driver instead.
- **Stray asterisks no longer appear in the dependency text.**

## [0.1.68] - 2026-08-08

### Added
Expand Down
99 changes: 71 additions & 28 deletions src/GamerGuardian/Services/CpuPlanStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
/// </summary>
public enum CcdServiceState
{
/// <summary>No service matching the optimizer was found at all.</summary>
/// <summary>The service list was read and nothing matching was in it.</summary>
NotInstalled,
/// <summary>The service list could not be read, so absence is not established.
/// Distinct from <see cref="NotInstalled"/> on purpose: the panel used to state
/// "isn't installed" as fact in both cases.</summary>
Unreadable,
/// <summary>Present and currently running.</summary>
Running,
/// <summary>Present, start mode is Automatic or Manual, not running right now.
Expand Down Expand Up @@ -53,7 +57,7 @@
public static CcdDependencyStatus DependencyStatus(
bool planActive, CcdServiceState service, bool? gameBarEnabled)
{
if (service == CcdServiceState.NotInstalled)
if (service is CcdServiceState.NotInstalled or CcdServiceState.Unreadable)
return CcdDependencyStatus.Unknown;
// A Disabled start mode is the only service state the user must act on.
// Idle is normal: the optimizer is demand-driven and sits stopped until a
Expand Down Expand Up @@ -83,23 +87,77 @@
{
try
{
foreach (var sc in System.ServiceProcess.ServiceController.GetServices())
using var services = Registry.LocalMachine.OpenSubKey(
@"SYSTEM\CurrentControlSet\Services", writable: false);
if (services is null) return new CcdServiceInfo(CcdServiceState.Unreadable, null, null);

// The registry is enumerated rather than ServiceController.GetServices(),
// which returns Win32 services only. AMD ships this as an INF driver
// package that registers BOTH a kernel driver ("amd3dvcache") and a
// user-mode helper ("amd3dvcacheSvc"), and on a machine where only the
// driver is registered GetServices() sees nothing and the panel claimed
// the whole routing stack was missing. This key holds every service type.
CcdServiceInfo? best = null;
foreach (var name in services.GetSubKeyNames())
{
using (sc)
{
if (!LooksLikeVCacheOptimizer(sc.ServiceName, sc.DisplayName)) continue;
if (!LooksLikeVCacheOptimizer(name, null)) continue;

using var k = services.OpenSubKey(name, writable: false);
if (k is null) continue;

var state = sc.Status == System.ServiceProcess.ServiceControllerStatus.Running
? CcdServiceState.Running
: IsDisabled(sc.ServiceName) ? CcdServiceState.Disabled : CcdServiceState.Idle;
var display = CleanDisplayName(k.GetValue("DisplayName") as string);
// Start: 4 == SERVICE_DISABLED.
bool disabled = k.GetValue("Start") is int start && start == 4;

return new CcdServiceInfo(state, sc.ServiceName, sc.DisplayName);
}
var state = disabled
? CcdServiceState.Disabled
: IsRunning(name) ? CcdServiceState.Running : CcdServiceState.Idle;

var info = new CcdServiceInfo(state, name, display);
// Prefer the most reassuring signal: something running beats
// something idle, and either beats a disabled entry.
if (best is null || Rank(state) > Rank(best.State)) best = info;
}

Check notice

Code scanning / CodeQL

Missed opportunity to use Where Note

This foreach loop
implicitly filters its target sequence
- consider filtering the sequence explicitly using '.Where(...)'.

return best ?? new CcdServiceInfo(CcdServiceState.NotInstalled, null, null);
}
catch { /* enumeration denied or unavailable -- fall through */ }
catch
{
// Read failed -- say so rather than reporting absence we did not observe.
return new CcdServiceInfo(CcdServiceState.Unreadable, null, null);
}
Comment on lines +124 to +128

static int Rank(CcdServiceState s) => s switch
{
CcdServiceState.Running => 3,
CcdServiceState.Idle => 2,
CcdServiceState.Disabled => 1,
_ => 0,
};
}

/// <summary>
/// Registry DisplayName values on INF-installed services are indirect strings
/// like <c>@oem46.inf,%amd3dvcacheSvc.DisplayName%;AMD 3D V-Cache Performance
/// Optimizer Service</c>. The readable fallback is the part after the last
/// semicolon. Pure, so it is unit-tested.
/// </summary>
public static string? CleanDisplayName(string? raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
if (!raw.StartsWith('@')) return raw;
int semi = raw.LastIndexOf(';');
return semi >= 0 && semi < raw.Length - 1 ? raw[(semi + 1)..].Trim() : null;
}

return new CcdServiceInfo(CcdServiceState.NotInstalled, null, null);
private static bool IsRunning(string serviceName)
{
try
{
using var sc = new System.ServiceProcess.ServiceController(serviceName);
return sc.Status == System.ServiceProcess.ServiceControllerStatus.Running;
}
catch { return false; }
}

/// <summary>Matches the optimizer by service name or display name. Pure, so the
Expand All @@ -118,21 +176,6 @@
haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
}

/// <summary>Start mode from the registry. ServiceController exposes StartType
/// only on .NET Core 3.0+ for some platforms, and reading the key is the same
/// registry-first approach the rest of the app uses.</summary>
private static bool IsDisabled(string serviceName)
{
try
{
using var k = Registry.LocalMachine.OpenSubKey(
$@"SYSTEM\CurrentControlSet\Services\{serviceName}", writable: false);
// 4 == SERVICE_DISABLED
return k?.GetValue("Start") is int start && start == 4;
}
catch { return false; }
}

/// <summary>
/// Whether Windows Game Mode is on — the signal the AMD optimizer's game
/// detection rides on.
Expand Down
18 changes: 12 additions & 6 deletions src/GamerGuardian/UI/SettingsWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1765,12 +1765,16 @@ private void BuildDependencyRows(CpuTuneResult r)
var found = svcInfo.ServiceName is { } n ? $" [{n}]" : "";
AddDependencyRow(svc switch
{
CcdServiceState.Running => $"✓ AMD 3D V-Cache Optimizer service: running{found}",
CcdServiceState.Running => $"✓ AMD 3D V-Cache Optimizer: running{found}",
// Auto-start and stopped is how this service idles; it starts when it has
// routing work. Saying so beats telling the user to fix a non-problem.
CcdServiceState.Idle => $"✓ AMD 3D V-Cache Optimizer service: installed, idle{found} -- normal; it starts when a game needs routing",
CcdServiceState.Disabled => $"⚠ AMD 3D V-Cache Optimizer service: disabled{found} -- set its start type back to Automatic",
_ => "— AMD 3D V-Cache Optimizer service: not found. This ships with the AMD *chipset* driver, which is a separate download from Adrenalin (the GPU driver).",
CcdServiceState.Idle => $"✓ AMD 3D V-Cache Optimizer: installed, idle{found} -- normal; it starts when a game needs routing",
CcdServiceState.Disabled => $"⚠ AMD 3D V-Cache Optimizer: disabled{found} -- set its start type back to Automatic",
CcdServiceState.Unreadable => "— AMD 3D V-Cache Optimizer: could not read the service list, so its state is unknown",
// Only needed on the Driver path. Naming the package matters: it is the
// "AMD Chipset Software" download, not Adrenalin, and inside it the
// component is listed as "AMD 3D V-Cache Performance Optimizer Driver".
_ => $"— AMD 3D V-Cache Optimizer: not installed. Only needed if you set CPPC to Driver; it comes with the \"AMD Chipset Software\" package (not the Adrenalin GPU driver). Not required on the recommended CPPC={CpuTuneCatalog.PreferredCppcValue} path.",
});

// Checkable: Windows Game Mode. Named for what it actually reads -- the row
Expand All @@ -1792,9 +1796,11 @@ private void BuildDependencyRows(CpuTuneResult r)
$"Checkable dependencies look good. Still set BIOS CPPC={CpuTuneCatalog.PreferredCppcValue} in your firmware -- the app can't read it, so it never claims full confirmation.",
CcdDependencyStatus.PartlyUnmet =>
"At least one dependency is unmet -- the optimized plan won't route games to the cache CCD until it's fixed.",
_ when svc == CcdServiceState.Unreadable =>
$"Couldn't read the service list, so the AMD optimizer's state is unknown. Setting BIOS CPPC={CpuTuneCatalog.PreferredCppcValue} routes games to the cache CCD regardless of it.",
_ =>
"The AMD 3D V-Cache Optimizer service isn't installed. It comes with the AMD chipset driver (separate from the Adrenalin GPU driver). Note that setting BIOS CPPC="
+ $"{CpuTuneCatalog.PreferredCppcValue} routes games to the cache CCD without depending on this service at all.",
$"Set BIOS CPPC={CpuTuneCatalog.PreferredCppcValue} and you're done -- that pins games to the cache CCD on its own, with no dependency on the AMD optimizer or on Xbox Game Bar detecting the game. "
+ "The optimizer is only required if you choose CPPC=Driver instead, and it ships in the \"AMD Chipset Software\" package rather than with Adrenalin.",
};
var summaryBlock = new System.Windows.Controls.TextBlock
{
Expand Down
59 changes: 58 additions & 1 deletion tests/GamerGuardian.Tests/CpuPlanStatusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void RealMachine_DetectorNeverThrowsAndReportsCoherently()
// not.
var info = CpuPlanStatus.ReadAmdVCacheService();

if (info.State == CcdServiceState.NotInstalled)
if (info.State is CcdServiceState.NotInstalled or CcdServiceState.Unreadable)
{
Assert.Null(info.ServiceName);
}
Expand All @@ -122,6 +122,63 @@ public void RealMachine_DetectorNeverThrowsAndReportsCoherently()
}
}

[Fact]
public void ServiceUnreadable_IsUnknownNotAbsent()
{
// The panel used to state "isn't installed" as fact when the read had simply
// failed. Unreadable and NotInstalled both mean "can't confirm the stack",
// but only one of them justifies telling the user to install something.
Assert.Equal(CcdDependencyStatus.Unknown,
CpuPlanStatus.DependencyStatus(planActive: true, CcdServiceState.Unreadable, gameBarEnabled: true));
}

[Fact]
public void Matcher_FindsTheKernelDriverAsWellAsTheWin32Service()
{
// AMD's INF package registers BOTH: a kernel driver "amd3dvcache" and a
// user-mode helper "amd3dvcacheSvc". ServiceController.GetServices() returns
// Win32 services only, so a machine carrying just the driver looked like it
// had no routing stack at all. Both must match.
Assert.True(CpuPlanStatus.LooksLikeVCacheOptimizer("amd3dvcache", null));
Assert.True(CpuPlanStatus.LooksLikeVCacheOptimizer("amd3dvcacheSvc", null));
}

// ---- Display names -----------------------------------------------------

[Fact]
public void CleanDisplayName_ResolvesTheIndirectStringInfInstalledServicesUse()
{
// Verified against the real key: INF-installed services store DisplayName as
// an indirect string whose readable fallback follows the last semicolon.
Assert.Equal("AMD 3D V-Cache Performance Optimizer Service",
CpuPlanStatus.CleanDisplayName(
"@oem46.inf,%amd3dvcacheSvc.DisplayName%;AMD 3D V-Cache Performance Optimizer Service"));
}

[Fact]
public void CleanDisplayName_LeavesAPlainNameAlone()
{
Assert.Equal("AMD 3D V-Cache Performance Optimizer Service",
CpuPlanStatus.CleanDisplayName("AMD 3D V-Cache Performance Optimizer Service"));
}

[Fact]
public void CleanDisplayName_ReturnsNullRatherThanAnUnresolvedIndirectString()
{
// Showing "@oem46.inf,%amd3dvcacheSvc.DisplayName%" in the panel would be
// worse than showing nothing.
Assert.Null(CpuPlanStatus.CleanDisplayName("@oem46.inf,%amd3dvcacheSvc.DisplayName%"));
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CleanDisplayName_HandlesMissingValues(string? raw)
{
Assert.Null(CpuPlanStatus.CleanDisplayName(raw));
}

[Fact]
public void Matcher_HandlesNulls()
{
Expand Down