Skip to content

Commit 9c71059

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/update-prompt-history
2 parents 86b6169 + 721d8dd commit 9c71059

3 files changed

Lines changed: 209 additions & 12 deletions

File tree

src/GamerGuardian/Services/CpuPlanDetails.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Globalization;
12
using GamerGuardian.Models;
3+
using GamerGuardian.Native;
24

35
namespace GamerGuardian.Services;
46

@@ -27,6 +29,74 @@ public static string BaseSummary(CpuTuneResult r) =>
2729
public static IReadOnlyList<string> Changes(CpuTuneResult r) =>
2830
r.Overrides.Select(o => o.Label).ToList();
2931

32+
/// <summary>One row of the side-by-side "Windows Balanced vs GamerGuardian"
33+
/// comparison: the setting's friendly name and its value under each plan.
34+
/// <paramref name="Differs"/> is true only when the base value is known AND
35+
/// differs from the GamerGuardian value, so the UI can highlight the real
36+
/// differences without falsely flagging a value it couldn't read.</summary>
37+
public sealed record PlanComparisonRow(string Setting, string WindowsValue, string GamerGuardianValue, bool Differs);
38+
39+
/// <summary>
40+
/// Builds the side-by-side comparison of the stock Windows base plan against the
41+
/// GamerGuardian tune, one row per processor setting the tune touches. The base
42+
/// (plugged-in) value is read through the injected <paramref name="readBaseAcValue"/>
43+
/// delegate — the UI passes a live Powrprof reader against the installed Balanced
44+
/// scheme; tests pass a stub. When a base value can't be read (setting hidden or
45+
/// scheme missing) the row shows "Windows default" and is not flagged as differing.
46+
/// </summary>
47+
public static IReadOnlyList<PlanComparisonRow> Comparison(
48+
CpuTuneResult r, Func<Guid, Guid, uint?> readBaseAcValue)
49+
{
50+
var rows = new List<PlanComparisonRow>(r.Overrides.Count);
51+
foreach (var o in r.Overrides)
52+
{
53+
var (name, fmt) = DescribeSetting(o.Setting);
54+
uint? baseVal = null;
55+
if (readBaseAcValue is not null)
56+
{
57+
try { baseVal = readBaseAcValue(o.Subgroup, o.Setting); }
58+
catch { baseVal = null; }
59+
}
60+
rows.Add(new PlanComparisonRow(
61+
Setting: name,
62+
WindowsValue: baseVal is uint w ? fmt(w) : "Windows default",
63+
GamerGuardianValue: fmt(o.Value),
64+
Differs: baseVal is uint b && b != o.Value));
65+
}
66+
return rows;
67+
}
68+
69+
/// <summary>Friendly name + value formatter for each processor power setting the
70+
/// tunes use. Keyed on the well-known setting GUID so it never drifts from what
71+
/// the catalog actually writes.</summary>
72+
private static (string name, Func<uint, string> fmt) DescribeSetting(Guid setting)
73+
{
74+
if (setting == Powrprof.SettingBoostMode) return ("Processor boost mode", BoostModeText);
75+
if (setting == Powrprof.SettingCoreParkingMinCores) return ("Core parking — minimum cores", Percent);
76+
if (setting == Powrprof.SettingCoreParkingMaxCores) return ("Core parking — maximum cores", Percent);
77+
if (setting == Powrprof.SettingPerfIncreaseThreshold) return ("Performance-increase threshold", Percent);
78+
if (setting == Powrprof.SettingIdleDemoteThreshold) return ("Idle-demote threshold", Percent);
79+
if (setting == Powrprof.SettingMinProcessorState) return ("Minimum processor state", Percent);
80+
if (setting == Powrprof.SettingMaxProcessorState) return ("Maximum processor state", Percent);
81+
return ("Processor setting", Raw);
82+
}
83+
84+
private static string Percent(uint v) => v.ToString(CultureInfo.InvariantCulture) + "%";
85+
private static string Raw(uint v) => v.ToString(CultureInfo.InvariantCulture);
86+
87+
/// <summary>Windows PERFBOOSTMODE value names.</summary>
88+
private static string BoostModeText(uint v) => v switch
89+
{
90+
0 => "Disabled",
91+
1 => "Enabled",
92+
2 => "Aggressive",
93+
3 => "Efficient Enabled",
94+
4 => "Efficient Aggressive",
95+
5 => "Aggressive at guaranteed",
96+
6 => "Efficient Aggressive at guaranteed",
97+
_ => v.ToString(CultureInfo.InvariantCulture),
98+
};
99+
30100
/// <summary>Why this specific recipe is a good fit for the detected CPU. Keyed
31101
/// on the parking strategy (the one dimension that actually changes the shape of
32102
/// the tune), with a generic-fallback and a hybrid-Intel special case.</summary>

src/GamerGuardian/UI/SettingsWindow.xaml.cs

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,22 +1493,22 @@ private void BuildPlanDetails(CpuTuneResult r)
14931493

14941494
PlanDetailsList.Children.Add(new System.Windows.Controls.TextBlock
14951495
{
1496-
Text = "Changes from stock (applied to both plugged-in and on-battery):",
1496+
Text = $"Side by side (plugged in) — bold values are what GamerGuardian changes:",
14971497
FontWeight = FontWeights.SemiBold,
14981498
FontSize = 12,
14991499
TextWrapping = TextWrapping.Wrap,
1500-
Margin = new Thickness(0, 10, 0, 0),
1500+
Margin = new Thickness(0, 12, 0, 4),
15011501
});
1502-
foreach (var change in CpuPlanDetails.Changes(r))
1503-
{
1504-
PlanDetailsList.Children.Add(new System.Windows.Controls.TextBlock
1505-
{
1506-
Text = "• " + change,
1507-
FontSize = 12,
1508-
TextWrapping = TextWrapping.Wrap,
1509-
Margin = new Thickness(8, 2, 0, 0),
1510-
});
1511-
}
1502+
1503+
// Read the stock values live from the installed base scheme so the "Windows"
1504+
// column reflects this machine rather than a hardcoded guess (the app never
1505+
// trusts hardcoded power values — see the Power Saver GUID gotcha).
1506+
var baseGuid = PowerPlanMonitor.ResolveBalancedBase();
1507+
Func<Guid, Guid, uint?> readBase = baseGuid == Guid.Empty
1508+
? (_, _) => null
1509+
: (sub, set) => Powrprof.ReadAcValue(baseGuid, sub, set);
1510+
var comparison = CpuPlanDetails.Comparison(r, readBase);
1511+
PlanDetailsList.Children.Add(BuildComparisonTable(comparison, r.BasePlanDisplayName));
15121512

15131513
PlanDetailsList.Children.Add(new System.Windows.Controls.TextBlock
15141514
{
@@ -1528,6 +1528,70 @@ private void BuildPlanDetails(CpuTuneResult r)
15281528
});
15291529
}
15301530

1531+
/// <summary>
1532+
/// Builds the "Windows base vs GamerGuardian" comparison as a 3-column grid.
1533+
/// A value that GamerGuardian changes from the stock value is shown bold so the
1534+
/// real differences stand out from settings it merely pins to the same value.
1535+
/// </summary>
1536+
private System.Windows.Controls.Grid BuildComparisonTable(
1537+
IReadOnlyList<CpuPlanDetails.PlanComparisonRow> rows, string baseName)
1538+
{
1539+
var grid = new System.Windows.Controls.Grid();
1540+
grid.ColumnDefinitions.Add(new System.Windows.Controls.ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
1541+
grid.ColumnDefinitions.Add(new System.Windows.Controls.ColumnDefinition { Width = new GridLength(120) });
1542+
grid.ColumnDefinitions.Add(new System.Windows.Controls.ColumnDefinition { Width = new GridLength(120) });
1543+
1544+
var secondary = (System.Windows.Media.Brush)FindResource("TextFillColorSecondaryBrush");
1545+
1546+
void AddCell(int row, int col, string text, FontWeight weight, System.Windows.Media.Brush? fg)
1547+
{
1548+
var tb = new System.Windows.Controls.TextBlock
1549+
{
1550+
Text = text,
1551+
FontSize = 12,
1552+
FontWeight = weight,
1553+
TextWrapping = TextWrapping.Wrap,
1554+
Margin = new Thickness(col == 0 ? 0 : 8, 3, 8, 3),
1555+
};
1556+
if (fg is not null) tb.Foreground = fg;
1557+
System.Windows.Controls.Grid.SetRow(tb, row);
1558+
System.Windows.Controls.Grid.SetColumn(tb, col);
1559+
grid.Children.Add(tb);
1560+
}
1561+
1562+
// Header row.
1563+
grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = GridLength.Auto });
1564+
AddCell(0, 0, "Setting", FontWeights.SemiBold, null);
1565+
AddCell(0, 1, $"Windows {baseName}", FontWeights.SemiBold, null);
1566+
AddCell(0, 2, "GamerGuardian", FontWeights.SemiBold, null);
1567+
1568+
// Separator under the header.
1569+
grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = GridLength.Auto });
1570+
var sep = new System.Windows.Controls.Border
1571+
{
1572+
Height = 1,
1573+
Background = (System.Windows.Media.Brush)FindResource("ControlStrokeColorDefaultBrush"),
1574+
Margin = new Thickness(0, 1, 0, 3),
1575+
};
1576+
System.Windows.Controls.Grid.SetRow(sep, 1);
1577+
System.Windows.Controls.Grid.SetColumn(sep, 0);
1578+
System.Windows.Controls.Grid.SetColumnSpan(sep, 3);
1579+
grid.Children.Add(sep);
1580+
1581+
for (int i = 0; i < rows.Count; i++)
1582+
{
1583+
int row = i + 2;
1584+
grid.RowDefinitions.Add(new System.Windows.Controls.RowDefinition { Height = GridLength.Auto });
1585+
AddCell(row, 0, rows[i].Setting, FontWeights.Normal, null);
1586+
AddCell(row, 1, rows[i].WindowsValue, FontWeights.Normal, secondary);
1587+
// Bold the GamerGuardian value only where it actually differs from stock.
1588+
AddCell(row, 2, rows[i].GamerGuardianValue,
1589+
rows[i].Differs ? FontWeights.SemiBold : FontWeights.Normal, null);
1590+
}
1591+
1592+
return grid;
1593+
}
1594+
15311595
private void BuildDependencyRows(CpuTuneResult r)
15321596
{
15331597
CcdDependencyList.Children.Clear();

tests/GamerGuardian.Tests/CpuPlanDetailsTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,67 @@ public void ResolveDesiredGuid_FreshPref_ResolvesToBalanced()
113113
var guid = GamerGuardian.Monitors.PowerPlanMonitor.ResolveDesiredGuid(new PowerPlanPref());
114114
Assert.Equal(GamerGuardian.Monitors.PowerPlanMonitor.Balanced, guid);
115115
}
116+
117+
// ---- Side-by-side comparison (Windows base vs GamerGuardian) ----
118+
119+
[Fact]
120+
public void Comparison_HasOneRowPerOverride_WithFriendlyNamesAndValues()
121+
{
122+
var r = Amd("9950X3D"); // boost=Aggressive(2), min cores=50, max cores=100
123+
// Derive each setting's GUID from the recipe's own overrides (Powrprof's GUIDs
124+
// are internal), then stub the base reader: boost=Enabled(1), min=100, max=100.
125+
Guid Setting(string labelPart) =>
126+
r.Overrides.First(o => o.Label.Contains(labelPart, StringComparison.OrdinalIgnoreCase)).Setting;
127+
var boostSetting = Setting("boost");
128+
var minSetting = Setting("min cores");
129+
var maxSetting = Setting("max cores");
130+
var rows = CpuPlanDetails.Comparison(r, (sub, set) =>
131+
set == boostSetting ? 1u :
132+
set == minSetting ? 100u :
133+
set == maxSetting ? 100u : (uint?)null);
134+
135+
Assert.Equal(r.Overrides.Count, rows.Count);
136+
137+
var boost = rows.Single(x => x.Setting.Contains("boost", StringComparison.OrdinalIgnoreCase));
138+
Assert.Equal("Enabled", boost.WindowsValue);
139+
Assert.Equal("Aggressive", boost.GamerGuardianValue);
140+
Assert.True(boost.Differs);
141+
142+
var minCores = rows.Single(x => x.Setting.Contains("minimum cores", StringComparison.OrdinalIgnoreCase));
143+
Assert.Equal("100%", minCores.WindowsValue);
144+
Assert.Equal("50%", minCores.GamerGuardianValue);
145+
Assert.True(minCores.Differs);
146+
147+
var maxCores = rows.Single(x => x.Setting.Contains("maximum cores", StringComparison.OrdinalIgnoreCase));
148+
Assert.Equal("100%", maxCores.WindowsValue);
149+
Assert.Equal("100%", maxCores.GamerGuardianValue);
150+
Assert.False(maxCores.Differs); // same value -> pinned, not a difference
151+
}
152+
153+
[Fact]
154+
public void Comparison_UnreadableBaseValue_ShowsWindowsDefault_AndNotFlagged()
155+
{
156+
var r = Amd("9950X3D");
157+
var rows = CpuPlanDetails.Comparison(r, (_, _) => null);
158+
159+
Assert.All(rows, x => Assert.Equal("Windows default", x.WindowsValue));
160+
Assert.All(rows, x => Assert.False(x.Differs));
161+
}
162+
163+
[Fact]
164+
public void Comparison_NullReader_DoesNotThrow()
165+
{
166+
var r = Amd("9800X3D");
167+
var rows = CpuPlanDetails.Comparison(r, null!);
168+
Assert.Equal(r.Overrides.Count, rows.Count);
169+
Assert.All(rows, x => Assert.Equal("Windows default", x.WindowsValue));
170+
}
171+
172+
[Fact]
173+
public void Comparison_ReaderThatThrows_IsSwallowedPerRow()
174+
{
175+
var r = Amd("9800X3D");
176+
var rows = CpuPlanDetails.Comparison(r, (_, _) => throw new InvalidOperationException("boom"));
177+
Assert.All(rows, x => Assert.Equal("Windows default", x.WindowsValue));
178+
}
116179
}

0 commit comments

Comments
 (0)