diff --git a/SynToolkit.SystemInformationTests/Program.cs b/SynToolkit.SystemInformationTests/Program.cs index cf711c1..642a5de 100644 --- a/SynToolkit.SystemInformationTests/Program.cs +++ b/SynToolkit.SystemInformationTests/Program.cs @@ -58,6 +58,11 @@ private static int Main() Run("Malformed legacy profiles are rejected", MalformedLegacyProfilesAreRejected); Run("Oversized legacy profiles are rejected", OversizedLegacyProfileIsRejected); Run("NVIDIA profile export preserves imported settings", NvidiaProfileExportRoundTrips); + Run("Identified desktop DIMMs stay separate", IdentifiedDesktopDimmsStaySeparate); + Run("CPU-Z memory timings are parsed", CpuZMemoryTimingsAreParsed); + Run("CPU-Z processor details are parsed", CpuZProcessorDetailsAreParsed); + Run("CPU-Z motherboard details are parsed", CpuZMainboardDetailsAreParsed); + Run("AMD 3D V-Cache is detected", Amd3dVCacheIsDetected); Run("GPU vendor classification matches PCI IDs and names", GpuVendorClassificationMatchesPciIdsAndNames); Run("GPU tab icon uses NVIDIA/AMD brands and falls back for Intel/unknown", GpuTabIconUsesVendorBrandsAndSafeFallback); Run("Primary GPU vendor prefers discrete NVIDIA then AMD then Intel", PrimaryGpuVendorPrefersDiscreteGpu); @@ -69,6 +74,199 @@ private static int Main() return _failures == 0 ? 0 : 1; } + private static void GpuVendorClassificationMatchesPciIdsAndNames() + { + Equal( + GpuVendor.Nvidia, + GpuVendorClassification.GetVendor("NVIDIA GeForce RTX 4070", @"PCI\VEN_10DE&DEV_2786"), + "NVIDIA PCI vendor IDs must classify as NVIDIA."); + Equal( + GpuVendor.Amd, + GpuVendorClassification.GetVendor("Radeon RX 7800 XT", string.Empty), + "Radeon adapter names must classify as AMD when the PCI ID is missing."); + Equal( + GpuVendor.Intel, + GpuVendorClassification.GetVendor("Intel(R) UHD Graphics", @"PCI\VEN_8086&DEV_46A6"), + "Intel PCI vendor IDs must classify as Intel."); + Equal( + GpuVendor.Unknown, + GpuVendorClassification.GetVendor("Microsoft Basic Display Adapter", @"PCI\VEN_1414"), + "The Microsoft Basic Display Adapter must not be treated as a real GPU."); + Equal( + GpuVendorClassification.DefaultGpuIconPath, + GpuVendorClassification.GetGpuTabIconPath(GpuVendor.Intel), + "Intel systems must keep the generic GPU navigation icon."); + } + + private static void PrimaryGpuVendorPrefersDiscreteGpu() + { + Equal( + GpuVendor.Nvidia, + GpuVendorClassification.GetPrimaryGpuVendor( + [ + ("Intel(R) UHD Graphics", GpuVendor.Intel), + ("NVIDIA GeForce RTX 4070", GpuVendor.Nvidia), + ]), + "A laptop with Intel integrated graphics and NVIDIA discrete graphics must prefer NVIDIA."); + Equal( + GpuVendor.Amd, + GpuVendorClassification.GetPrimaryGpuVendor( + [ + ("Intel(R) Iris Xe Graphics", GpuVendor.Intel), + ("AMD Radeon RX 7600M", GpuVendor.Amd), + ]), + "A laptop with Intel integrated graphics and AMD discrete graphics must prefer AMD."); + } + + private static void HagsClassificationDistinguishesStates() + { + Equal( + HagsSupportState.NotSupportedByWindowsVersion, + HagsDetection.Classify(18363, null), + "Windows builds before version 2004 must report HAGS as unsupported by Windows."); + Equal( + HagsSupportState.NotSupportedByGpuOrDriver, + HagsDetection.Classify(HagsDetection.MinimumWindowsBuild, null), + "A missing HwSchMode on a supported Windows build means the GPU or driver does not support HAGS."); + Equal( + HagsSupportState.SupportedDisabled, + HagsDetection.Classify(26100, 1), + "HwSchMode=1 must be treated as supported but disabled."); + Equal( + HagsSupportState.SupportedEnabled, + HagsDetection.Classify(26100, 2), + "HwSchMode=2 must be treated as supported and enabled."); + Equal( + HagsSupportState.Unknown, + HagsDetection.Classify(26100, null, registryReadFailed: true), + "Registry read failures must remain unknown rather than being mislabeled as unsupported."); + } + + private static void FragmentedOnboardMemoryChipsAreLabeled() + { + MemoryModuleSpec[] firmwareEntries = Enumerable.Range(0, 8) + .Select(_ => new MemoryModuleSpec(null, 2UL * 1024 * 1024 * 1024, 4800)) + .ToArray(); + + IReadOnlyList normalized = MemoryModuleInventoryNormalizer.Normalize( + firmwareEntries, + 16UL * 1024 * 1024 * 1024 - 320UL * 1024 * 1024); + + Equal(8, normalized.Count, "Individual onboard chips should remain separate rows."); + True( + normalized.All(module => module.Manufacturer == "Onboard Memory Chip"), + "Each unidentified onboard chip should receive the onboard-memory label."); + True( + normalized.All(module => module.CapacityBytes == 2UL * 1024 * 1024 * 1024 && module.SpeedMHz == 4800), + "Each onboard chip must retain its own capacity and reported speed."); + } + + private static void IdentifiedDesktopDimmsStaySeparate() + { + MemoryModuleSpec[] desktopDimms = + { + new("Gold Key Technology Co Ltd", 8UL * 1024 * 1024 * 1024, 3200, IsMemoryStick: true), + new("Gold Key Technology Co Ltd", 8UL * 1024 * 1024 * 1024, 3200, IsMemoryStick: true) + }; + + IReadOnlyList normalized = MemoryModuleInventoryNormalizer.Normalize( + desktopDimms, + 16UL * 1024 * 1024 * 1024); + + Equal(2, normalized.Count, "Normal identified DIMMs must remain separate rows."); + Equal("Slot 1", normalized[0].SlotLabel, "The first stick must receive Slot 1."); + Equal("Slot 2", normalized[1].SlotLabel, "The second stick must receive Slot 2."); + } + + private static void CpuZMemoryTimingsAreParsed() + { + const string report = "Memory Type DDR5\r\n" + + "CAS# latency (CL) 40.0\r\n" + + "RAS# to CAS# delay (tRCD) 40\r\n" + + "RAS# Precharge (tRP) 40\r\n" + + "Cycle Time (tRAS) 77\r\n"; + + CpuZMemoryTimings? timings = CpuZMemoryTimingParser.TryParse(report); + + Equal("DDR5", timings?.MemoryType, "CPU-Z's memory type should be retained."); + Equal("CL40 40-40-40-77", timings?.TimingText, "CPU-Z's live primary timings should be formatted consistently."); + } + + private static void CpuZProcessorDetailsAreParsed() + { + const string report = "Processors Information\r\n" + + "-------------------------------------------------------------------------\r\n" + + "Socket 1\t\tID = 0\r\n" + + "\tNumber of cores\t\t14 (max 14)\r\n" + + "\tNumber of threads\t20 (max 20)\r\n" + + "\tHybrid\t\tyes, 2 coresets\r\n" + + "\tCore Set 0\t\tP-Cores, 6 cores, 12 threads\r\n" + + "\tCore Set 1\t\tE-Cores, 8 cores, 8 threads\r\n" + + "\tManufacturer\t\tGenuineIntel\r\n" + + "\tName\t\tIntel Core i5 13500\r\n" + + "\tCodename\t\tRaptor Lake\r\n" + + "\tPackage (platform ID)\tSocket 1700 LGA\r\n" + + "\tTechnology\t\t10 nm\r\n" + + "\tCPUID\t\t6.F.2\r\n" + + "\tCore Stepping\t\tC0\r\n" + + "\tInstructions sets\tMMX, SSE, AVX2\r\n" + + "\tTDP Limit\t\t65.0 Watts\r\n" + + "\tTjmax\t\t100.0 °C\r\n" + + "\tBase frequency (cores)\t99.8 MHz\r\n" + + "\tL1 Data cache\t\t6 x 48 KB (12-way) + 8 x 32 KB (8-way)\r\n" + + "\tL1 Instruction cache\t6 x 32 KB (8-way) + 8 x 64 KB (8-way)\r\n" + + "\tL2 cache\t\t6 x 1.25 MB (10-way) + 2 x 2 MB (16-way)\r\n" + + "\tL3 cache\t\t24 MB (12-way)\r\n" + + "\tMax turbo ratio\t\t48x\r\n" + + "\tMin operating ratio\t4x\r\n" + + "\tRatio 1 P-Core\t\t48x\r\n" + + "\tRatio 1 E-Core\t\t35x\r\n" + + "\r\nThread dumps\r\n"; + + CpuZProcessorDetails? details = CpuZProcessorDetailsParser.TryParse(report); + + Equal("Raptor Lake", details?.Codename, "CPU-Z's codename should be retained."); + Equal(6, details?.PerformanceCores?.Cores, "P-core count should be parsed."); + Equal(8, details?.EfficientCores?.Cores, "E-core count should be parsed."); + Equal(4790.4m, details?.PerformanceCores?.MaximumFrequencyMHz, "P-core maximum should use the P-core ratio."); + Equal(3493m, details?.EfficientCores?.MaximumFrequencyMHz, "E-core maximum should use the E-core ratio."); + Equal(399.2m, details?.MinimumFrequencyMHz, "Minimum operating frequency should use its ratio."); + Equal("6 × 48 KB + 8 × 32 KB", details?.L1DataCache, "Cache associativity should be omitted while preserving the hybrid cache layout."); + Equal("100 \u00B0C", details?.TemperatureLimit, "Temperature output must use a valid degree symbol."); + } + private static void CpuZMainboardDetailsAreParsed() + { + const string report = "Chipset\r\n" + + "Northbridge\t\tIntel Alder Lake rev. 02\r\n" + + "Southbridge\t\tIntel B660 rev. 11\r\n" + + "Bus Specification\t\tPCI-Express 4.0 (16.0 GT/s)\r\n" + + "Graphic Interface\t\tPCI-Express 5.0\r\n" + + "Mainboard Model\t\tB660M DS3H AX DDR4 (0x00000444 - 0x8461EA80)\r\n" + + "LPCIO Vendor\t\tITE\r\n" + + "LPCIO Model\t\tIT8689\r\n"; + + CpuZMainboardDetails? details = CpuZMainboardDetailsParser.TryParse(report); + + Equal("B660M DS3H AX DDR4", details?.Model, "CPU-Z's board model should omit the internal hardware ID."); + Equal("Intel Alder Lake rev. 02", details?.Northbridge, "CPU-Z's northbridge should be retained."); + Equal("Intel B660 rev. 11", details?.Southbridge, "CPU-Z's southbridge should be retained."); + Equal("PCI-Express 4.0 (16.0 GT/s)", details?.BusSpecification, "CPU-Z's mainboard bus should be retained."); + Equal("ITE", details?.LpcioVendor, "CPU-Z's LPCIO vendor should be retained."); + Equal("IT8689", details?.LpcioModel, "CPU-Z's LPCIO model should be retained."); + } + private static void Amd3dVCacheIsDetected() + { + const string report = "Processors Information\r\n" + + "\tManufacturer\t\tAuthenticAMD\r\n" + + "\tName\t\tAMD Ryzen 7 7800X3D\r\n" + + "\tL3 cache\t\t96 MB (16-way)\r\n" + + "\r\nThread dumps\r\n"; + + CpuZProcessorDetails? details = CpuZProcessorDetailsParser.TryParse(report); + + True(details?.HasAmd3dVCache == true, "An AMD X3D processor must expose its 3D V-Cache designation."); + Equal("96 MB", details?.L3Cache, "The AMD L3 cache capacity should remain visible."); + } private static void OfficialRegistryPath() => Equal( @"SOFTWARE\AME\Playbooks\Applied", diff --git a/SynToolkit.SystemInformationTests/SynToolkit.SystemInformationTests.csproj b/SynToolkit.SystemInformationTests/SynToolkit.SystemInformationTests.csproj index f1537bc..7a5c23f 100644 --- a/SynToolkit.SystemInformationTests/SynToolkit.SystemInformationTests.csproj +++ b/SynToolkit.SystemInformationTests/SynToolkit.SystemInformationTests.csproj @@ -12,6 +12,7 @@ + @@ -20,6 +21,12 @@ + + + + + + diff --git a/SynToolkit/Commands/ConfigurationButtonsCommand/OpenSystemProtectionCommand.cs b/SynToolkit/Commands/ConfigurationButtonsCommand/OpenSystemProtectionCommand.cs new file mode 100644 index 0000000..b0d52c4 --- /dev/null +++ b/SynToolkit/Commands/ConfigurationButtonsCommand/OpenSystemProtectionCommand.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using SynToolkit.Utils; + +namespace SynToolkit.Commands.ConfigurationButtonsCommand +{ + internal sealed class OpenSystemProtectionCommand : AsyncCommandBase + { + protected override async Task ExecuteAsync(object parameter) + { + await Task.Run(() => ProcessHelper.StartShellExecute("SystemPropertiesProtection.exe")); + } + } +} \ No newline at end of file diff --git a/SynToolkit/HostBuilder/AddViewModelsHostBuilderExtensions.cs b/SynToolkit/HostBuilder/AddViewModelsHostBuilderExtensions.cs index a437fb5..77b97fa 100644 --- a/SynToolkit/HostBuilder/AddViewModelsHostBuilderExtensions.cs +++ b/SynToolkit/HostBuilder/AddViewModelsHostBuilderExtensions.cs @@ -149,6 +149,7 @@ private static IHostBuilder AddConfigurationButtonItemViewModels(this IHostBuild ["InstallOpenShell"] = new(buttonCommand = new InstallOpenShellCommand(), App.GetValueFromItemList("InstallOpenShell"), App.GetValueFromItemList("InstallOpenShell", true), ConfigurationType.StartMenuSubMenu, "ms-appx:///assets/Icons/Windows.png"), ["DiskCleanup"] = new(buttonCommand = new DiskCleanupCommand(), App.GetValueFromItemList("DiskCleanup"), App.GetValueFromItemList("DiskCleanup", true), ConfigurationType.Troubleshooting, "ms-appx:///assets/Icons/DiskCleanup.png"), + ["SystemRestore"] = new(buttonCommand = new OpenSystemProtectionCommand(), App.GetValueFromItemList("SystemRestore"), App.GetValueFromItemList("SystemRestore", true), ConfigurationType.Troubleshooting, "ms-appx:///assets/Icons/Security.png"), ["RepairWindowsInstaller"] = new(buttonCommand = new RepairWindowsInstallerCommand(), App.GetValueFromItemList("FixErrors"), App.GetValueFromItemList("RepairWindowsInstaller"), ConfigurationType.Troubleshooting, "ms-appx:///assets/Icons/Update.png"), ["RepairWinComponent"] = new(buttonCommand = new RepairWindowsComponentsCommand(), App.GetValueFromItemList("FixErrors"), App.GetValueFromItemList("RepairWinComponent"), ConfigurationType.Troubleshooting, "ms-appx:///assets/Icons/Update.png"), ["TelemetryComponents"] = new(buttonCommand = new TelemetryComponentsCommand(), App.GetValueFromItemList("FixErrors"), App.GetValueFromItemList("TelemetryComponents"), ConfigurationType.Troubleshooting, "ms-appx:///assets/Icons/Update.png"), diff --git a/SynToolkit/MainWindow.xaml.cs b/SynToolkit/MainWindow.xaml.cs index a8fb911..e9d6299 100644 --- a/SynToolkit/MainWindow.xaml.cs +++ b/SynToolkit/MainWindow.xaml.cs @@ -246,7 +246,7 @@ public void LoadText() LearnMoreBtn.Content = App.GetValueFromItemList("LearnMore"); // Navigation Items - Home.Content = App.GetValueFromItemList("Home_HeaderText"); + Home.Content = App.GetValueFromItemList("Home"); InstallerText.Text = App.GetValueFromItemList("Installer"); PowerPlansText.Text = App.GetValueFromItemList("PowerPlans"); AdjustmentsText.Text = App.GetValueFromItemList("Adjustments"); diff --git a/SynToolkit/Models/SystemSpecs.cs b/SynToolkit/Models/SystemSpecs.cs index 753b9a0..999aef9 100644 --- a/SynToolkit/Models/SystemSpecs.cs +++ b/SynToolkit/Models/SystemSpecs.cs @@ -13,7 +13,14 @@ public sealed record GpuSpec( string IconPath, GpuVendor Vendor); - public sealed record MemoryModuleSpec(string? Manufacturer, ulong CapacityBytes, uint? SpeedMHz); + public sealed record MemoryModuleSpec( + string? Manufacturer, + ulong CapacityBytes, + uint? SpeedMHz, + string? MemoryType = null, + string? SlotLabel = null, + string? TimingText = null, + bool IsMemoryStick = false); public sealed record StorageDriveSpec(string Model, ulong SizeBytes, string? MediaType, string? InterfaceType); @@ -28,6 +35,8 @@ public sealed record NetworkAdapterSpec( public sealed record MotherboardSpec(string? Manufacturer, string? Product); + public sealed record MotherboardDetail(string Label, string Value); + public sealed record SystemSpecsSnapshot( CpuSpec? Cpu, System.Collections.Generic.IReadOnlyList Gpus, @@ -40,4 +49,4 @@ public sealed record SystemSpecsSnapshot( string WindowsDisplayVersion, string WindowsBuild, string Architecture); -} \ No newline at end of file +} diff --git a/SynToolkit/Services/CpuUsageSampler.cs b/SynToolkit/Services/CpuUsageSampler.cs new file mode 100644 index 0000000..2237ff6 --- /dev/null +++ b/SynToolkit/Services/CpuUsageSampler.cs @@ -0,0 +1,218 @@ +#nullable enable + +using System; +using System.Runtime.InteropServices; + +namespace SynToolkit.Services +{ + internal readonly record struct CpuLiveMetrics(uint? UtilizationPercent, decimal? AverageFrequencyMHz); + + /// + /// Samples only Windows' aggregate CPU counters. It does not start a process, issue WMI + /// queries, or change any power setting, so it is safe to call from the Specs timer. + /// + internal sealed class CpuUsageSampler + { + private const int ProcessorInformation = 11; + private const uint PdhFmtDouble = 0x00000200; + private IntPtr _performanceQuery; + private IntPtr _processorFrequencyCounter; + private IntPtr _processorPerformanceCounter; + private bool _isPerformanceQueryUnavailable; + private ulong? _previousIdleTime; + private ulong? _previousKernelTime; + private ulong? _previousUserTime; + + internal CpuLiveMetrics Sample() => new(ReadUtilizationPercent(), ReadLiveFrequencyMHz()); + + private uint? ReadUtilizationPercent() + { + if (!GetSystemTimes(out FileTime idleTime, out FileTime kernelTime, out FileTime userTime)) + { + return null; + } + + ulong idle = ToUInt64(idleTime); + ulong kernel = ToUInt64(kernelTime); + ulong user = ToUInt64(userTime); + if (!_previousIdleTime.HasValue || !_previousKernelTime.HasValue || !_previousUserTime.HasValue) + { + _previousIdleTime = idle; + _previousKernelTime = kernel; + _previousUserTime = user; + return null; + } + + ulong totalDelta = (kernel - _previousKernelTime.Value) + (user - _previousUserTime.Value); + ulong idleDelta = idle - _previousIdleTime.Value; + _previousIdleTime = idle; + _previousKernelTime = kernel; + _previousUserTime = user; + if (totalDelta == 0 || idleDelta > totalDelta) + { + return null; + } + + return (uint)Math.Clamp(Math.Round((totalDelta - idleDelta) * 100d / totalDelta), 0, 100); + } + + private decimal? ReadLiveFrequencyMHz() + { + if (!EnsurePerformanceQuery() || + PdhCollectQueryData(_performanceQuery) != 0 || + !TryReadCounterValue(_processorFrequencyCounter, out double nominalFrequencyMHz) || + !TryReadCounterValue(_processorPerformanceCounter, out double processorPerformancePercent)) + { + return ReadAveragePowerInformationFrequencyMHz(); + } + + try + { + return Convert.ToDecimal(nominalFrequencyMHz * processorPerformancePercent / 100d); + } + catch (OverflowException) + { + return ReadAveragePowerInformationFrequencyMHz(); + } + } + + private bool EnsurePerformanceQuery() + { + if (_performanceQuery != IntPtr.Zero) + { + return true; + } + + if (_isPerformanceQueryUnavailable || + PdhOpenQueryW(null, IntPtr.Zero, out IntPtr query) != 0) + { + _isPerformanceQueryUnavailable = true; + return false; + } + + if (PdhAddEnglishCounterW(query, @"\Processor Information(_Total)\Processor Frequency", IntPtr.Zero, out IntPtr frequencyCounter) != 0 || + PdhAddEnglishCounterW(query, @"\Processor Information(_Total)\% Processor Performance", IntPtr.Zero, out IntPtr performanceCounter) != 0) + { + PdhCloseQuery(query); + _isPerformanceQueryUnavailable = true; + return false; + } + + _performanceQuery = query; + _processorFrequencyCounter = frequencyCounter; + _processorPerformanceCounter = performanceCounter; + return true; + } + + private static bool TryReadCounterValue(IntPtr counter, out double value) + { + value = 0; + if (PdhGetFormattedCounterValue(counter, PdhFmtDouble, out _, out PdhFormattedCounterValue formattedValue) != 0 || + formattedValue.CStatus != 0 || + double.IsNaN(formattedValue.DoubleValue) || + double.IsInfinity(formattedValue.DoubleValue) || + formattedValue.DoubleValue <= 0) + { + return false; + } + + value = formattedValue.DoubleValue; + return true; + } + private static decimal? ReadAveragePowerInformationFrequencyMHz() + { + int processorCount = Math.Max(Environment.ProcessorCount, 1); + int structureSize = Marshal.SizeOf(); + IntPtr buffer = Marshal.AllocHGlobal(checked(structureSize * processorCount)); + try + { + if (CallNtPowerInformation(ProcessorInformation, IntPtr.Zero, 0, buffer, checked((uint)(structureSize * processorCount))) != 0) + { + return null; + } + + ulong totalMHz = 0; + int validProcessors = 0; + for (int index = 0; index < processorCount; index++) + { + IntPtr current = IntPtr.Add(buffer, checked(index * structureSize)); + ProcessorPowerInformation processor = Marshal.PtrToStructure(current); + if (processor.CurrentMhz == 0) + { + continue; + } + + totalMHz += processor.CurrentMhz; + validProcessors++; + } + + return validProcessors == 0 ? null : (decimal)totalMHz / validProcessors; + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + private static ulong ToUInt64(FileTime value) => ((ulong)value.HighDateTime << 32) | value.LowDateTime; + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetSystemTimes(out FileTime idleTime, out FileTime kernelTime, out FileTime userTime); + + [DllImport("PowrProf.dll")] + private static extern uint CallNtPowerInformation( + int informationLevel, + IntPtr inputBuffer, + uint inputBufferLength, + IntPtr outputBuffer, + uint outputBufferLength); + + [DllImport("pdh.dll", CharSet = CharSet.Unicode)] + private static extern uint PdhOpenQueryW(string? dataSource, IntPtr userData, out IntPtr query); + + [DllImport("pdh.dll", CharSet = CharSet.Unicode)] + private static extern uint PdhAddEnglishCounterW(IntPtr query, string fullCounterPath, IntPtr userData, out IntPtr counter); + + [DllImport("pdh.dll")] + private static extern uint PdhCollectQueryData(IntPtr query); + + [DllImport("pdh.dll")] + private static extern uint PdhGetFormattedCounterValue( + IntPtr counter, + uint format, + out uint type, + out PdhFormattedCounterValue value); + + [DllImport("pdh.dll")] + private static extern uint PdhCloseQuery(IntPtr query); + + [StructLayout(LayoutKind.Sequential)] + private struct FileTime + { + internal uint LowDateTime; + internal uint HighDateTime; + } + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessorPowerInformation + { + internal uint Number; + internal uint MaxMhz; + internal uint CurrentMhz; + internal uint MhzLimit; + internal uint MaxIdleState; + internal uint CurrentIdleState; + } + + [StructLayout(LayoutKind.Explicit)] + private struct PdhFormattedCounterValue + { + [FieldOffset(0)] + internal uint CStatus; + + [FieldOffset(8)] + internal double DoubleValue; + } + } +} diff --git a/SynToolkit/Services/CpuZMainboardDetailsParser.cs b/SynToolkit/Services/CpuZMainboardDetailsParser.cs new file mode 100644 index 0000000..fffec17 --- /dev/null +++ b/SynToolkit/Services/CpuZMainboardDetailsParser.cs @@ -0,0 +1,71 @@ +#nullable enable + +using System; + +namespace SynToolkit.Services +{ + internal sealed record CpuZMainboardDetails( + string? Model, + string? Northbridge, + string? Southbridge, + string? BusSpecification, + string? GraphicsInterface, + string? LpcioVendor, + string? LpcioModel); + + internal static class CpuZMainboardDetailsParser + { + internal static CpuZMainboardDetails? TryParse(string report) + { + if (string.IsNullOrWhiteSpace(report)) + { + return null; + } + + CpuZMainboardDetails details = new( + CleanModel(ReadValue(report, "Mainboard Model")), + ReadValue(report, "Northbridge"), + ReadValue(report, "Southbridge"), + ReadValue(report, "Bus Specification"), + ReadValue(report, "Graphic Interface"), + ReadValue(report, "LPCIO Vendor"), + ReadValue(report, "LPCIO Model")); + + return details.Model is null + && details.Northbridge is null + && details.Southbridge is null + && details.LpcioVendor is null + ? null + : details; + } + + private static string? ReadValue(string report, string label) + { + foreach (string line in report.Split(["\r\n", "\n"], StringSplitOptions.None)) + { + string trimmed = line.TrimStart(); + if (!trimmed.StartsWith(label, StringComparison.OrdinalIgnoreCase) + || (trimmed.Length > label.Length && !char.IsWhiteSpace(trimmed[label.Length]))) + { + continue; + } + + string value = trimmed[label.Length..].Trim(); + return string.IsNullOrWhiteSpace(value) ? null : value; + } + + return null; + } + + private static string? CleanModel(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + int hardwareIdStart = value.IndexOf(" (0x", StringComparison.OrdinalIgnoreCase); + return (hardwareIdStart >= 0 ? value[..hardwareIdStart] : value).Trim(); + } + } +} \ No newline at end of file diff --git a/SynToolkit/Services/CpuZMemoryReportService.cs b/SynToolkit/Services/CpuZMemoryReportService.cs new file mode 100644 index 0000000..1ea7151 --- /dev/null +++ b/SynToolkit/Services/CpuZMemoryReportService.cs @@ -0,0 +1,87 @@ +#nullable enable + +using System; +using System.Diagnostics; +using System.IO; + +namespace SynToolkit.Services +{ + internal sealed record CpuZHardwareReport(CpuZMemoryTimings? MemoryTimings, CpuZProcessorDetails? ProcessorDetails, CpuZMainboardDetails? MainboardDetails); + + /// + /// Reads one hidden CPU-Z report per app session. Memory, processor, and motherboard details share that report, + /// which prevents a second CPU-Z process and keeps the initial Specs snapshot responsive. + /// + internal static class CpuZMemoryReportService + { + private const int ReportTimeoutMilliseconds = 25_000; + private static readonly Lazy CurrentReport = new(ReadCurrentReport); + + internal static CpuZMemoryTimings? GetCurrentTimings() => CurrentReport.Value?.MemoryTimings; + + internal static CpuZProcessorDetails? GetCurrentProcessorDetails() => CurrentReport.Value?.ProcessorDetails; + + internal static CpuZMainboardDetails? GetCurrentMainboardDetails() => CurrentReport.Value?.MainboardDetails; + + private static CpuZHardwareReport? ReadCurrentReport() + { + string executablePath = Path.Combine(AppContext.BaseDirectory, "assets", "Tools", "cpuz_x64.exe"); + if (!File.Exists(executablePath)) + { + return null; + } + + string reportBasePath = Path.Combine( + Path.GetTempPath(), + "SynToolkit-CpuZ-" + Guid.NewGuid().ToString("N")); + string reportPath = reportBasePath + ".txt"; + try + { + using Process process = new(); + process.StartInfo = new ProcessStartInfo + { + FileName = executablePath, + WorkingDirectory = Path.GetDirectoryName(executablePath), + UseShellExecute = false, + CreateNoWindow = true + }; + process.StartInfo.ArgumentList.Add("-txt=" + reportBasePath); + + if (!process.Start() || !process.WaitForExit(ReportTimeoutMilliseconds) || !File.Exists(reportPath)) + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + + return null; + } + + string report = File.ReadAllText(reportPath); + return new CpuZHardwareReport( + CpuZMemoryTimingParser.TryParse(report), + CpuZProcessorDetailsParser.TryParse(report), + CpuZMainboardDetailsParser.TryParse(report)); + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] CPU-Z report was unavailable."); + return null; + } + finally + { + try + { + if (File.Exists(reportPath)) + { + File.Delete(reportPath); + } + } + catch (IOException) + { + // The temporary report is harmless if a third-party scanner still holds it. + } + } + } + } +} \ No newline at end of file diff --git a/SynToolkit/Services/CpuZMemoryTimingParser.cs b/SynToolkit/Services/CpuZMemoryTimingParser.cs new file mode 100644 index 0000000..c059ce9 --- /dev/null +++ b/SynToolkit/Services/CpuZMemoryTimingParser.cs @@ -0,0 +1,70 @@ +#nullable enable + +using System; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace SynToolkit.Services +{ + internal sealed record CpuZMemoryTimings(string? MemoryType, string? TimingText); + + internal static class CpuZMemoryTimingParser + { + private static readonly Regex FirstNumberPattern = new(@"\d+(?:[\.,]\d+)?", RegexOptions.Compiled); + + internal static CpuZMemoryTimings? TryParse(string report) + { + if (string.IsNullOrWhiteSpace(report)) + { + return null; + } + + string? memoryType = ReadValue(report, "Memory Type"); + string? casLatency = ReadClockValue(report, "CAS# latency (CL)"); + string? rasToCasDelay = ReadClockValue(report, "RAS# to CAS# delay (tRCD)"); + string? rasPrecharge = ReadClockValue(report, "RAS# Precharge (tRP)"); + string? cycleTime = ReadClockValue(report, "Cycle Time (tRAS)"); + string? timingText = casLatency is null + ? null + : rasToCasDelay is not null && rasPrecharge is not null && cycleTime is not null + ? $"CL{casLatency} {casLatency}-{rasToCasDelay}-{rasPrecharge}-{cycleTime}" + : $"CL{casLatency}"; + + return string.IsNullOrWhiteSpace(memoryType) && timingText is null + ? null + : new CpuZMemoryTimings(memoryType, timingText); + } + + private static string? ReadValue(string report, string label) + { + foreach (string line in report.Split(["\r\n", "\n"], StringSplitOptions.None)) + { + string trimmed = line.TrimStart(); + if (trimmed.StartsWith(label, StringComparison.OrdinalIgnoreCase)) + { + string value = trimmed[label.Length..].Trim(); + return string.IsNullOrWhiteSpace(value) ? null : value; + } + } + + return null; + } + + private static string? ReadClockValue(string report, string label) + { + string? rawValue = ReadValue(report, label); + Match match = rawValue is null ? Match.Empty : FirstNumberPattern.Match(rawValue); + if (!match.Success || + !decimal.TryParse( + match.Value.Replace(',', '.'), + NumberStyles.AllowDecimalPoint, + CultureInfo.InvariantCulture, + out decimal value)) + { + return null; + } + + return value.ToString("0.##", CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/SynToolkit/Services/CpuZProcessorDetailsParser.cs b/SynToolkit/Services/CpuZProcessorDetailsParser.cs new file mode 100644 index 0000000..8f49e68 --- /dev/null +++ b/SynToolkit/Services/CpuZProcessorDetailsParser.cs @@ -0,0 +1,195 @@ +#nullable enable + +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace SynToolkit.Services +{ + internal sealed record CpuZCoreSet(string Name, int Cores, int Threads, decimal? MaximumFrequencyMHz); + + internal sealed record CpuZProcessorDetails( + string? Manufacturer, + string? Codename, + string? Socket, + string? Technology, + string? Cpuid, + string? Stepping, + string? InstructionSets, + string? ThermalDesignPower, + string? TemperatureLimit, + decimal? MinimumFrequencyMHz, + decimal? MaximumFrequencyMHz, + string? L1DataCache, + string? L1InstructionCache, + string? L2Cache, + string? L3Cache, + CpuZCoreSet? PerformanceCores, + CpuZCoreSet? EfficientCores, + bool HasAmd3dVCache); + + internal static class CpuZProcessorDetailsParser + { + private static readonly Regex FirstNumberPattern = new(@"\d+(?:[\.,]\d+)?", RegexOptions.Compiled); + private static readonly Regex CoreSetPattern = new( + @"^\s*Core Set \d+\s+(?[PE]-Cores),\s*(?\d+) cores,\s*(?\d+) threads", + RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase); + private static readonly Regex CoreRatioPattern = new( + @"^\s*Ratio \d+\s+(?[PE])-Core(?:s)?\s+(?\d+(?:[\.,]\d+)?)x", + RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase); + private static readonly Regex CacheDetailPattern = new(@"\s*\([^)]*\)", RegexOptions.Compiled); + private static readonly Regex CacheMultiplierPattern = new(@"\s+x\s+", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex RepeatedWhitespacePattern = new(@"\s+", RegexOptions.Compiled); + + internal static CpuZProcessorDetails? TryParse(string report) + { + if (string.IsNullOrWhiteSpace(report)) + { + return null; + } + + string processorSection = ExtractProcessorSection(report); + string? manufacturer = ReadValue(processorSection, "Manufacturer"); + string? name = ReadValue(processorSection, "Name"); + string? specification = ReadValue(processorSection, "Specification"); + decimal? baseFrequencyMHz = ReadNumber(processorSection, "Base frequency (cores)"); + decimal? minimumFrequencyMHz = Multiply(baseFrequencyMHz, ReadNumber(processorSection, "Min operating ratio")); + decimal? maximumFrequencyMHz = Multiply( + baseFrequencyMHz, + ReadNumber(processorSection, "Max turbo ratio") ?? ReadNumber(processorSection, "Max non-turbo ratio")); + CpuZCoreSet? performanceCores = ReadCoreSet(processorSection, "P", baseFrequencyMHz); + CpuZCoreSet? efficientCores = ReadCoreSet(processorSection, "E", baseFrequencyMHz); + bool isAmd = (manufacturer?.Contains("AMD", StringComparison.OrdinalIgnoreCase) ?? false) + || (name?.Contains("AMD", StringComparison.OrdinalIgnoreCase) ?? false) + || (specification?.Contains("AMD", StringComparison.OrdinalIgnoreCase) ?? false); + bool hasAmd3dVCache = isAmd && ( + name?.Contains("X3D", StringComparison.OrdinalIgnoreCase) == true + || specification?.Contains("X3D", StringComparison.OrdinalIgnoreCase) == true + || processorSection.Contains("3D V-Cache", StringComparison.OrdinalIgnoreCase)); + + bool hasDetails = manufacturer is not null + || name is not null + || maximumFrequencyMHz.HasValue + || performanceCores is not null + || efficientCores is not null + || ReadValue(processorSection, "L3 cache") is not null; + if (!hasDetails) + { + return null; + } + + return new CpuZProcessorDetails( + manufacturer, + ReadValue(processorSection, "Codename"), + ReadValue(processorSection, "Package (platform ID)"), + ReadValue(processorSection, "Technology"), + ReadValue(processorSection, "CPUID"), + ReadValue(processorSection, "Core Stepping"), + ReadValue(processorSection, "Instructions sets"), + ReadValue(processorSection, "TDP Limit"), + FormatTemperature(ReadValue(processorSection, "Tjmax")), + minimumFrequencyMHz, + maximumFrequencyMHz, + SimplifyCache(ReadValue(processorSection, "L1 Data cache")), + SimplifyCache(ReadValue(processorSection, "L1 Instruction cache")), + SimplifyCache(ReadValue(processorSection, "L2 cache")), + SimplifyCache(ReadValue(processorSection, "L3 cache")), + performanceCores, + efficientCores, + hasAmd3dVCache); + } + + private static CpuZCoreSet? ReadCoreSet(string processorSection, string corePrefix, decimal? baseFrequencyMHz) + { + Match coreSetMatch = CoreSetPattern.Matches(processorSection) + .Cast() + .FirstOrDefault(match => match.Groups["name"].Value.StartsWith(corePrefix, StringComparison.OrdinalIgnoreCase)) + ?? Match.Empty; + if (!coreSetMatch.Success + || !int.TryParse(coreSetMatch.Groups["cores"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out int cores) + || !int.TryParse(coreSetMatch.Groups["threads"].Value, NumberStyles.None, CultureInfo.InvariantCulture, out int threads)) + { + return null; + } + + decimal? maximumFrequencyMHz = CoreRatioPattern.Matches(processorSection) + .Cast() + .Where(match => match.Groups["name"].Value.Equals(corePrefix, StringComparison.OrdinalIgnoreCase)) + .Select(match => ParseDecimal(match.Groups["ratio"].Value)) + .Where(ratio => ratio.HasValue) + .Select(ratio => Multiply(baseFrequencyMHz, ratio)) + .Where(frequency => frequency.HasValue) + .Select(frequency => frequency!.Value) + .DefaultIfEmpty() + .Max(); + + return new CpuZCoreSet( + coreSetMatch.Groups["name"].Value.ToUpperInvariant(), + cores, + threads, + maximumFrequencyMHz == 0 ? null : maximumFrequencyMHz); + } + + private static string ExtractProcessorSection(string report) + { + int start = report.IndexOf("Processors Information", StringComparison.OrdinalIgnoreCase); + if (start < 0) + { + return report; + } + + int end = report.IndexOf("Thread dumps", start, StringComparison.OrdinalIgnoreCase); + return end < 0 ? report[start..] : report[start..end]; + } + + private static string? ReadValue(string report, string label) + { + foreach (string line in report.Split(["\r\n", "\n"], StringSplitOptions.None)) + { + string trimmed = line.TrimStart(); + if (trimmed.StartsWith(label, StringComparison.OrdinalIgnoreCase)) + { + string value = trimmed[label.Length..].Trim(); + return string.IsNullOrWhiteSpace(value) ? null : value; + } + } + + return null; + } + + private static decimal? ReadNumber(string report, string label) => ParseDecimal(ReadValue(report, label)); + + private static decimal? ParseDecimal(string? value) + { + Match match = value is null ? Match.Empty : FirstNumberPattern.Match(value); + return match.Success + && decimal.TryParse(match.Value.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal parsed) + ? parsed + : null; + } + + private static decimal? Multiply(decimal? first, decimal? second) => first.HasValue && second.HasValue + ? first.Value * second.Value + : null; + + private static string? FormatTemperature(string? value) + { + decimal? temperature = ParseDecimal(value); + return temperature.HasValue + ? temperature.Value.ToString("0.##", CultureInfo.InvariantCulture) + " \u00B0C" + : null; + } + private static string? SimplifyCache(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + return RepeatedWhitespacePattern.Replace( + CacheMultiplierPattern.Replace(CacheDetailPattern.Replace(value, string.Empty), " × "), + " ").Trim(); + } + } +} \ No newline at end of file diff --git a/SynToolkit/Services/DiscordPresenceService.cs b/SynToolkit/Services/DiscordPresenceService.cs index 5b58e35..f5b60f2 100644 --- a/SynToolkit/Services/DiscordPresenceService.cs +++ b/SynToolkit/Services/DiscordPresenceService.cs @@ -50,7 +50,7 @@ public bool TryStart(string applicationId, string largeImageKey) _presence = new RichPresence { - Details = "Using SynToolkit", + Details = "Using the best Toolkit", State = "Configuring Windows", Timestamps = Timestamps.Now, Assets = assets, diff --git a/SynToolkit/Services/MemoryModuleInventoryNormalizer.cs b/SynToolkit/Services/MemoryModuleInventoryNormalizer.cs new file mode 100644 index 0000000..5d5f165 --- /dev/null +++ b/SynToolkit/Services/MemoryModuleInventoryNormalizer.cs @@ -0,0 +1,73 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using SynToolkit.Models; + +namespace SynToolkit.Services +{ + /// + /// Normalizes firmware memory inventories for the Specs display. Some laptops expose each + /// soldered DRAM chip as a separate unidentified Win32_PhysicalMemory entry rather than a + /// module; those entries retain their individual capacity while receiving a useful onboard-memory label. + /// + internal static class MemoryModuleInventoryNormalizer + { + private const int MinimumFragmentCount = 4; + private const ulong Megabyte = 1024UL * 1024UL; + + internal static IReadOnlyList Normalize( + IEnumerable memoryModules, + ulong totalMemoryBytes) + { + List modules = memoryModules.ToList(); + if (ShouldLabelAsOnboardMemoryChips(modules, totalMemoryBytes)) + { + modules = modules + .Select(module => module with { Manufacturer = "Onboard Memory Chip" }) + .ToList(); + } + + int nextSlotNumber = 1; + return modules + .Select(module => module.IsMemoryStick + ? module with { SlotLabel = $"Slot {nextSlotNumber++}" } + : module) + .ToList(); + } + + private static bool ShouldLabelAsOnboardMemoryChips( + IReadOnlyList modules, + ulong totalMemoryBytes) + { + if (totalMemoryBytes == 0 || modules.Count < MinimumFragmentCount || + modules.Any(module => module.CapacityBytes == 0 || module.SpeedMHz is null || + !IsUnidentifiedManufacturer(module.Manufacturer))) + { + return false; + } + + if (modules.Select(module => module.CapacityBytes).Distinct().Skip(1).Any() || + modules.Select(module => module.SpeedMHz).Distinct().Skip(1).Any()) + { + return false; + } + + ulong combinedCapacity = modules.Aggregate(0UL, (total, module) => total + module.CapacityBytes); + ulong difference = combinedCapacity >= totalMemoryBytes + ? combinedCapacity - totalMemoryBytes + : totalMemoryBytes - combinedCapacity; + ulong tolerance = Math.Max(512UL * Megabyte, totalMemoryBytes / 20); + return difference <= tolerance; + } + + private static bool IsUnidentifiedManufacturer(string? manufacturer) + { + return string.IsNullOrWhiteSpace(manufacturer) || + manufacturer.Trim().Equals("Unknown", StringComparison.OrdinalIgnoreCase) || + manufacturer.Trim().Equals("Not specified", StringComparison.OrdinalIgnoreCase) || + manufacturer.Trim().Equals("To be filled by O.E.M.", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/SynToolkit/Services/SystemSpecsService.cs b/SynToolkit/Services/SystemSpecsService.cs index b685dfb..8bdbfd3 100644 --- a/SynToolkit/Services/SystemSpecsService.cs +++ b/SynToolkit/Services/SystemSpecsService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Management; using Microsoft.Win32; @@ -25,11 +26,13 @@ public static SystemSpecsSnapshot GetSnapshot(ISystemInformationService systemIn { SystemInformationSnapshot windowsInfo = systemInformationService.Detect(); + ulong totalMemoryBytes = GetTotalMemoryBytes(); + return new SystemSpecsSnapshot( GetCpu(), GetGpus(), - GetTotalMemoryBytes(), - GetMemoryModules(), + totalMemoryBytes, + GetMemoryModules(totalMemoryBytes), GetStorageDrives(), GetNetworkAdapters(), GetMotherboard(), @@ -77,7 +80,6 @@ private static IReadOnlyList GetGpus() string name = item["Name"] as string ?? "Unknown GPU"; string pnpDeviceId = item["PNPDeviceID"] as string ?? string.Empty; string? driverVersion = item["DriverVersion"] as string; - ulong? adapterRam = item["AdapterRAM"] is object rawAdapterRam ? Convert.ToUInt64(rawAdapterRam) : null; @@ -193,12 +195,12 @@ private static ulong GetTotalMemoryBytes() return 0; } - private static IReadOnlyList GetMemoryModules() + private static IReadOnlyList GetMemoryModules(ulong totalMemoryBytes) { List modules = new(); try { - using ManagementObjectSearcher searcher = new("SELECT Manufacturer, Capacity, Speed, ConfiguredClockSpeed FROM Win32_PhysicalMemory"); + using ManagementObjectSearcher searcher = new("SELECT Manufacturer, Capacity, Speed, ConfiguredClockSpeed, SMBIOSMemoryType, FormFactor FROM Win32_PhysicalMemory"); foreach (ManagementBaseObject item in searcher.Get()) { using (item) @@ -207,7 +209,16 @@ private static IReadOnlyList GetMemoryModules() ulong capacity = Convert.ToUInt64(item["Capacity"] ?? 0UL); uint? configuredClockSpeed = ReadPositiveUInt32(item["ConfiguredClockSpeed"]); uint? reportedSpeed = ReadPositiveUInt32(item["Speed"]); - modules.Add(new MemoryModuleSpec(manufacturer, capacity, configuredClockSpeed ?? reportedSpeed)); + uint? smbiosMemoryType = ReadUInt32(item["SMBIOSMemoryType"]); + ushort? formFactor = ReadUInt16(item["FormFactor"]); + modules.Add(new MemoryModuleSpec( + manufacturer, + capacity, + configuredClockSpeed ?? reportedSpeed, + GetMemoryTechnology(smbiosMemoryType), + null, + null, + IsMemoryStick(formFactor))); } } } @@ -216,7 +227,25 @@ private static IReadOnlyList GetMemoryModules() App.logger.Warn(exception, "[Specs] Unable to read memory module information via WMI."); } - return modules; + return MemoryModuleInventoryNormalizer.Normalize(modules, totalMemoryBytes); + } + + /// + /// Adds optional live timing data after the WMI snapshot has already been shown. + /// CPU-Z can take several seconds to generate its report, so it must not delay the + /// rest of the Specs tab. + /// + public static IReadOnlyList AddCurrentMemoryTimingDetails( + IReadOnlyList modules) + { + CpuZMemoryTimings? cpuZTimings = CpuZMemoryReportService.GetCurrentTimings(); + return modules + .Select(module => module with + { + MemoryType = module.MemoryType ?? cpuZTimings?.MemoryType, + TimingText = cpuZTimings?.TimingText + }) + .ToList(); } private static IReadOnlyList GetStorageDrives() @@ -341,6 +370,20 @@ private static bool IsUserFacingPhysicalNetworkAdapter(ManagementBaseObject adap _ => "Inactive" }; + private static string? GetMemoryTechnology(uint? smbiosMemoryType) => smbiosMemoryType switch + { + 18 => "DDR1", + 19 => "DDR2", + 20 => "DDR2 FB-DIMM", + 24 => "DDR3", + 26 => "DDR4", + 34 => "DDR5", + 35 => "LPDDR5", + 36 => "LPDDR5X", + _ => null // CPU-Z provides a fallback for future values, including DDR6. + }; + + private static bool IsMemoryStick(ushort? formFactor) => formFactor is 7 or 8 or 11 or 12 or 13 or 15; private static uint? ReadPositiveUInt32(object? value) { if (value is null) @@ -373,6 +416,476 @@ private static bool IsUserFacingPhysicalNetworkAdapter(ManagementBaseObject adap ulong? speed = ReadPositiveUInt64(value); return speed is null or >= (ulong)long.MaxValue ? null : speed; } + /// + /// Reads the fuller firmware-provided motherboard inventory only after the user opens + /// the Motherboard expander. Empty physical sockets are shown only when firmware reports + /// them; Windows does not expose a dependable inventory for every unpopulated NVMe socket. + /// + public static IReadOnlyList GetMotherboardDetails() + { + List details = new(); + CpuZMainboardDetails? cpuZMainboard = CpuZMemoryReportService.GetCurrentMainboardDetails(); + AddBaseboardDetails(details, cpuZMainboard); + AddCpuZMainboardDetails(details, cpuZMainboard); + AddFirmwareDetails(details); + AddProcessorSocketDetails(details, CpuZMemoryReportService.GetCurrentProcessorDetails()?.Socket); + AddMemorySlotDetails(details); + AddExpansionSlotDetails(details); + AddNvmeDeviceDetails(details); + AddChassisDetails(details); + return details; + } + + private static void AddBaseboardDetails(ICollection details, CpuZMainboardDetails? cpuZMainboard) + { + try + { + using ManagementObjectSearcher searcher = new( + "SELECT Manufacturer, Product, Version, SerialNumber, Tag FROM Win32_BaseBoard"); + foreach (ManagementBaseObject item in searcher.Get()) + { + using (item) + { + AddDetail(details, "Board manufacturer", ReadText(item, "Manufacturer")); + AddDetail(details, "Board model", cpuZMainboard?.Model ?? ReadText(item, "Product")); + AddDetail(details, "Board revision", ReadText(item, "Version")); + AddDetail(details, "Board serial number", ReadText(item, "SerialNumber")); + AddDetail(details, "Board asset tag", ReadText(item, "Tag")); + return; + } + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read detailed motherboard information via WMI."); + } + } + + private static void AddCpuZMainboardDetails(ICollection details, CpuZMainboardDetails? cpuZMainboard) + { + if (cpuZMainboard is null) + { + return; + } + + AddDetail(details, "Northbridge", cpuZMainboard.Northbridge); + AddDetail(details, "Southbridge", cpuZMainboard.Southbridge); + AddDetail(details, "Mainboard bus", cpuZMainboard.BusSpecification); + AddDetail(details, "Graphics interface", cpuZMainboard.GraphicsInterface); + AddDetail( + details, + "LPCIO", + string.Join(" \u00B7 ", new[] { cpuZMainboard.LpcioVendor, cpuZMainboard.LpcioModel } + .Where(value => !string.IsNullOrWhiteSpace(value)))); + } + private static void AddFirmwareDetails(ICollection details) + { + try + { + using ManagementObjectSearcher searcher = new( + "SELECT Manufacturer, SMBIOSBIOSVersion, Version, ReleaseDate, SMBIOSMajorVersion, SMBIOSMinorVersion FROM Win32_BIOS"); + foreach (ManagementBaseObject item in searcher.Get()) + { + using (item) + { + AddDetail(details, "BIOS manufacturer", ReadText(item, "Manufacturer")); + AddDetail( + details, + "BIOS version", + ReadText(item, "SMBIOSBIOSVersion") ?? ReadText(item, "Version")); + AddDetail(details, "BIOS release date", FormatWmiDate(ReadText(item, "ReleaseDate"))); + + uint? major = ReadPositiveUInt32(item["SMBIOSMajorVersion"]); + uint? minor = ReadPositiveUInt32(item["SMBIOSMinorVersion"]); + if (major.HasValue) + { + AddDetail( + details, + "SMBIOS version", + minor.HasValue + ? $"{major.Value}.{minor.Value}" + : major.Value.ToString(CultureInfo.InvariantCulture)); + } + + break; + } + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read BIOS information via WMI."); + } + + AddDetail(details, "Firmware mode", GetFirmwareMode()); + } + + private static void AddProcessorSocketDetails(ICollection details, string? preferredSocket) + { + if (!string.IsNullOrWhiteSpace(preferredSocket)) + { + AddDetail(details, "CPU socket", preferredSocket); + return; + } + + try + { + using ManagementObjectSearcher searcher = new("SELECT SocketDesignation FROM Win32_Processor"); + string sockets = string.Join( + ", ", + searcher.Get() + .Cast() + .Select(item => + { + using (item) + { + return ReadText(item, "SocketDesignation"); + } + }) + .Where(socket => !string.IsNullOrWhiteSpace(socket)) + .Distinct(StringComparer.OrdinalIgnoreCase)); + AddDetail(details, "CPU socket", sockets); + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read CPU socket information via WMI."); + } + } + + private static void AddMemorySlotDetails(ICollection details) + { + int? slotCount = null; + try + { + using ManagementObjectSearcher arraySearcher = new("SELECT MemoryDevices FROM Win32_PhysicalMemoryArray"); + int reportedSlotCount = 0; + foreach (ManagementBaseObject item in arraySearcher.Get()) + { + using (item) + { + uint? memoryDevices = ReadPositiveUInt32(item["MemoryDevices"]); + if (memoryDevices.HasValue) + { + reportedSlotCount += checked((int)memoryDevices.Value); + } + } + } + + if (reportedSlotCount > 0) + { + slotCount = reportedSlotCount; + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read memory slot count via WMI."); + } + + List populatedSlots = new(); + try + { + using ManagementObjectSearcher memorySearcher = new( + "SELECT DeviceLocator, BankLabel, Capacity, SMBIOSMemoryType, ConfiguredClockSpeed, Speed FROM Win32_PhysicalMemory"); + int unnamedModuleNumber = 0; + foreach (ManagementBaseObject item in memorySearcher.Get()) + { + using (item) + { + string locator = ReadText(item, "DeviceLocator") ?? $"Memory module {++unnamedModuleNumber}"; + string? bank = ReadText(item, "BankLabel"); + ulong capacity = item["Capacity"] is object rawCapacity ? Convert.ToUInt64(rawCapacity) : 0UL; + uint? speed = ReadPositiveUInt32(item["ConfiguredClockSpeed"]) ?? ReadPositiveUInt32(item["Speed"]); + string? memoryType = GetMemoryTechnology(ReadUInt32(item["SMBIOSMemoryType"])); + + List parts = new() { locator }; + if (!string.IsNullOrWhiteSpace(bank) && !string.Equals(bank, locator, StringComparison.OrdinalIgnoreCase)) + { + parts.Add(bank!); + } + + if (capacity > 0) + { + parts.Add(FormatGigabytes(capacity)); + } + + if (!string.IsNullOrWhiteSpace(memoryType)) + { + parts.Add(memoryType!); + } + + if (speed.HasValue) + { + parts.Add($"{speed.Value:N0} MT/s"); + } + + populatedSlots.Add(string.Join(" \u00B7 ", parts)); + } + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read populated memory slots via WMI."); + } + + if (slotCount.HasValue || populatedSlots.Count > 0) + { + string summary = slotCount.HasValue + ? $"{populatedSlots.Count} / {slotCount.Value} populated" + : $"{populatedSlots.Count} populated"; + AddDetail(details, "Memory slots", summary); + } + + foreach (string populatedSlot in populatedSlots.OrderBy(slot => slot, StringComparer.OrdinalIgnoreCase)) + { + AddDetail(details, "Memory slot", populatedSlot); + } + } + + private static void AddExpansionSlotDetails(ICollection details) + { + List<(string Label, string Value, bool InUse)> pcieSlots = new(); + List<(string Label, string Value, bool InUse)> m2Slots = new(); + try + { + using ManagementObjectSearcher searcher = new( + "SELECT SlotDesignation, SlotType, CurrentUsage, MaxDataWidth FROM Win32_SystemSlot"); + foreach (ManagementBaseObject item in searcher.Get()) + { + using (item) + { + string designation = ReadText(item, "SlotDesignation") ?? "Unnamed slot"; + ushort? slotType = ReadUInt16(item["SlotType"]); + ushort? maxDataWidth = ReadUInt16(item["MaxDataWidth"]); + ushort? currentUsage = ReadUInt16(item["CurrentUsage"]); + string? pcieType = GetPcieSlotType(slotType); + bool isM2 = designation.Contains("M.2", StringComparison.OrdinalIgnoreCase) + || designation.Contains("M2", StringComparison.OrdinalIgnoreCase) + || designation.Contains("NVME", StringComparison.OrdinalIgnoreCase); + bool isPcie = pcieType is not null + || designation.Contains("PCI", StringComparison.OrdinalIgnoreCase); + if (!isM2 && !isPcie) + { + continue; + } + + string interfaceText = pcieType ?? "PCI Express"; + if (!interfaceText.Contains(" x", StringComparison.OrdinalIgnoreCase) + && maxDataWidth is > 0) + { + interfaceText += $" x{maxDataWidth.Value}"; + } + + string value = string.Join(" \u00B7 ", new[] + { + designation, + interfaceText, + GetSlotUsage(currentUsage) + }); + (isM2 ? m2Slots : pcieSlots).Add((isM2 ? "M.2 slot" : "PCIe slot", value, currentUsage == 4)); + } + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read expansion slots via WMI."); + } + + foreach ((string label, string value, _) in pcieSlots + .OrderByDescending(slot => slot.InUse) + .ThenBy(slot => slot.Value, StringComparer.OrdinalIgnoreCase)) + { + AddDetail(details, label, value); + } + + foreach ((string label, string value, _) in m2Slots + .OrderByDescending(slot => slot.InUse) + .ThenBy(slot => slot.Value, StringComparer.OrdinalIgnoreCase)) + { + AddDetail(details, label, value); + } + } + + private static void AddNvmeDeviceDetails(ICollection details) + { + try + { + using ManagementObjectSearcher searcher = new( + "SELECT Model, FirmwareRevision, PNPDeviceID, InterfaceType FROM Win32_DiskDrive"); + foreach (ManagementBaseObject item in searcher.Get()) + { + using (item) + { + string? model = ReadText(item, "Model"); + string? pnpDeviceId = ReadText(item, "PNPDeviceID"); + string? interfaceType = ReadText(item, "InterfaceType"); + bool isNvme = (pnpDeviceId?.Contains("NVME", StringComparison.OrdinalIgnoreCase) ?? false) + || (model?.Contains("NVME", StringComparison.OrdinalIgnoreCase) ?? false) + || string.Equals(interfaceType, "NVMe", StringComparison.OrdinalIgnoreCase); + if (!isNvme) + { + continue; + } + + List parts = new() { model ?? "Unknown NVMe drive" }; + string? firmware = ReadText(item, "FirmwareRevision"); + if (!string.IsNullOrWhiteSpace(firmware)) + { + parts.Add($"Firmware {firmware}"); + } + + AddDetail(details, "NVMe drive", string.Join(" \u00B7 ", parts)); + } + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read NVMe storage information via WMI."); + } + } + + private static void AddChassisDetails(ICollection details) + { + try + { + using ManagementObjectSearcher searcher = new("SELECT ChassisTypes FROM Win32_SystemEnclosure"); + foreach (ManagementBaseObject item in searcher.Get()) + { + using (item) + { + if (item["ChassisTypes"] is not Array chassisTypes || chassisTypes.Length == 0) + { + continue; + } + + ushort chassisType = Convert.ToUInt16(chassisTypes.GetValue(0), CultureInfo.InvariantCulture); + AddDetail(details, "System form factor", GetChassisType(chassisType)); + return; + } + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read chassis information via WMI."); + } + } + + private static void AddDetail(ICollection details, string label, string? value) + { + string? cleanValue = CleanText(value); + if (!string.IsNullOrWhiteSpace(cleanValue)) + { + details.Add(new MotherboardDetail(label, cleanValue)); + } + } + + private static string? ReadText(ManagementBaseObject item, string propertyName) => CleanText(item[propertyName]); + + private static string? CleanText(object? rawValue) + { + string value = Convert.ToString(rawValue, CultureInfo.InvariantCulture)?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(value) + || string.Equals(value, "Unknown", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "None", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "N/A", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "Not Specified", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "Default string", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "System Serial Number", StringComparison.OrdinalIgnoreCase) + || value.StartsWith("To Be Filled By O.E.M.", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return value; + } + + private static string? FormatWmiDate(string? value) + { + if (string.IsNullOrWhiteSpace(value) || value.Length < 8) + { + return null; + } + + return DateTime.TryParseExact( + value[..8], + "yyyyMMdd", + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out DateTime releaseDate) + ? releaseDate.ToString("MMM d, yyyy", CultureInfo.InvariantCulture) + : value; + } + + private static string? GetFirmwareMode() + { + try + { + using RegistryKey? controlKey = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control"); + return ReadUInt32(controlKey?.GetValue("PEFirmwareType")) switch + { + 1 => "Legacy BIOS", + 2 => "UEFI", + _ => null + }; + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Unable to read firmware mode from the registry."); + return null; + } + } + + private static string GetSlotUsage(ushort? currentUsage) => currentUsage switch + { + 3 => "Available", + 4 => "In use", + 5 => "Unavailable", + _ => "Usage not reported" + }; + + private static string? GetPcieSlotType(ushort? slotType) + { + if (slotType is >= 165 and <= 170) + { + return FormatPcieSlotType(null, slotType.Value - 165); + } + + if (slotType is >= 171 and <= 200) + { + int offset = slotType.Value - 171; + return FormatPcieSlotType(2 + offset / 6, offset % 6); + } + + return null; + } + + private static string FormatPcieSlotType(int? generation, int widthIndex) + { + int[] widths = { 0, 1, 2, 4, 8, 16 }; + string value = generation.HasValue ? $"PCIe Gen {generation.Value}" : "PCI Express"; + return widths[widthIndex] == 0 ? value : $"{value} x{widths[widthIndex]}"; + } + + private static string? GetChassisType(ushort chassisType) => chassisType switch + { + 3 => "Desktop", + 4 => "Low-profile desktop", + 5 => "Pizza-box desktop", + 6 => "Mini tower", + 7 => "Tower", + 8 => "Portable", + 9 => "Laptop", + 10 => "Notebook", + 14 => "Sub-notebook", + 30 => "Tablet", + 31 => "Convertible", + 32 => "Detachable", + _ => null + }; + + private static string FormatGigabytes(ulong bytes) + { + const double gigabyte = 1024d * 1024 * 1024; + return (bytes / gigabyte).ToString("0.##", CultureInfo.InvariantCulture) + " GB"; + } private static MotherboardSpec? GetMotherboard() { try diff --git a/SynToolkit/ViewModels/SpecsPageViewModel.cs b/SynToolkit/ViewModels/SpecsPageViewModel.cs index 302364b..8de0ae8 100644 --- a/SynToolkit/ViewModels/SpecsPageViewModel.cs +++ b/SynToolkit/ViewModels/SpecsPageViewModel.cs @@ -12,7 +12,21 @@ namespace SynToolkit.ViewModels { - public sealed record GpuSpecDisplay(string Name, string VramText, string DriverVersionText, string IconPath); + public sealed class GpuSpecDisplay + { + public GpuSpecDisplay(string name, string vramText, string driverVersionText, string iconPath) + { + Name = name; + VramText = vramText; + DriverVersionText = driverVersionText; + IconPath = iconPath; + } + + public string Name { get; } + public string VramText { get; } + public string DriverVersionText { get; } + public string IconPath { get; } + } public sealed record MemoryModuleDisplay(string ManufacturerText, string CapacityText); @@ -20,6 +34,9 @@ public sealed record StorageDriveDisplay(string Model, string SizeText, string T public sealed record NetworkAdapterDisplay(string Name, string ManufacturerText, string StatusText, string DetailsText); + public sealed record CpuDetailDisplay(string Label, string Value); + public sealed record MotherboardDetailDisplay(string Label, string Value); + /// /// Drives the Specs tab: a read-only snapshot of CPU, GPU, memory, storage, motherboard, /// and Windows identity via SystemSpecsService. Purely informational — makes no changes. @@ -27,6 +44,11 @@ public sealed record NetworkAdapterDisplay(string Name, string ManufacturerText, public partial class SpecsPageViewModel : ObservableObject { private readonly ISystemInformationService _systemInformationService; + private readonly CpuUsageSampler _cpuUsageSampler = new(); + private decimal? _minimumObservedCpuFrequencyMHz; + private decimal? _maximumObservedCpuFrequencyMHz; + private bool _areMotherboardDetailsLoaded; + private bool _areMotherboardDetailsLoading; [ObservableProperty] public partial bool IsLoading { get; set; } = true; @@ -43,12 +65,24 @@ public partial class SpecsPageViewModel : ObservableObject [ObservableProperty] public partial string CpuDetailsText { get; set; } = string.Empty; + [ObservableProperty] + public partial string CpuUtilizationText { get; set; } = "Calculating..."; + + [ObservableProperty] + public partial string CpuCurrentFrequencyText { get; set; } = "Detecting..."; + + [ObservableProperty] + public partial string CpuObservedFrequencyText { get; set; } = "Collecting..."; + [ObservableProperty] public partial string MotherboardText { get; set; } = string.Empty; [ObservableProperty] public partial string TotalMemoryText { get; set; } = string.Empty; + [ObservableProperty] + public partial string MemoryDescriptionText { get; set; } = string.Empty; + [ObservableProperty] public partial string WindowsText { get; set; } = string.Empty; @@ -63,6 +97,8 @@ public string NetworkSummaryText [ObservableProperty] public partial string GraphicsHeaderIcon { get; set; } = GpuDetectionService.DefaultGpuIconPath; + public ObservableCollection CpuDetails { get; } = new(); + public ObservableCollection MotherboardDetails { get; } = new(); public ObservableCollection Gpus { get; } = new(); public ObservableCollection MemoryModules { get; } = new(); public ObservableCollection StorageDrives { get; } = new(); @@ -82,15 +118,15 @@ public async Task LoadAsync() SystemSpecsSnapshot snapshot = await Task.Run(() => SystemSpecsService.GetSnapshot(_systemInformationService)); CpuName = snapshot.Cpu?.Name ?? "Unknown CPU"; - CpuDetailsText = snapshot.Cpu is null - ? string.Empty - : $"{snapshot.Cpu.Cores} cores, {snapshot.Cpu.LogicalProcessors} logical processors, {snapshot.Cpu.MaxClockSpeedMHz / 1000.0:0.00} GHz"; + CpuDetailsText = CreateCpuSummary(snapshot.Cpu) + " · Loading detailed CPU information..."; + DisplayCpuDetails(snapshot.Cpu, null); MotherboardText = snapshot.Motherboard is null ? "Unknown" - : string.Join(" ", new[] { snapshot.Motherboard.Manufacturer, snapshot.Motherboard.Product }.Where(part => !string.IsNullOrWhiteSpace(part))); + : string.Join(" \u00B7 ", new[] { snapshot.Motherboard.Manufacturer, snapshot.Motherboard.Product }.Where(part => !string.IsNullOrWhiteSpace(part))); TotalMemoryText = FormatBytes(snapshot.TotalMemoryBytes); + MemoryDescriptionText = TotalMemoryText; WindowsText = $"{snapshot.WindowsProductName} ({snapshot.WindowsDisplayVersion}, Build {snapshot.WindowsBuild}, {snapshot.Architecture})"; Gpus.Clear(); @@ -105,16 +141,7 @@ public async Task LoadAsync() GraphicsHeaderIcon = GpuDetectionService.GetPrimaryIconPath(snapshot.Gpus); - MemoryModules.Clear(); - foreach (MemoryModuleSpec module in snapshot.MemoryModules) - { - MemoryModules.Add(new MemoryModuleDisplay( - string.IsNullOrWhiteSpace(module.Manufacturer) ? "Unknown manufacturer" : module.Manufacturer!, - module.SpeedMHz.HasValue - ? $"{FormatBytes(module.CapacityBytes)} · {module.SpeedMHz.Value:N0} MHz" - : FormatBytes(module.CapacityBytes))); - } - + DisplayMemoryModules(snapshot.MemoryModules); NetworkAdapters.Clear(); foreach (NetworkAdapterSpec adapter in snapshot.NetworkAdapters) @@ -152,6 +179,10 @@ public async Task LoadAsync() string typeText = string.Join(" / ", new[] { drive.MediaType, drive.InterfaceType }.Where(part => !string.IsNullOrWhiteSpace(part))); StorageDrives.Add(new StorageDriveDisplay(drive.Model, FormatBytes(drive.SizeBytes), typeText)); } + + MemoryDescriptionText = $"{TotalMemoryText} · Loading CAS Latency & timings..."; + _ = LoadMemoryTimingDetailsAsync(snapshot.MemoryModules); + _ = LoadCpuDetailsAsync(snapshot.Cpu); } catch (Exception exception) { @@ -166,6 +197,247 @@ public async Task LoadAsync() } + public async Task LoadMotherboardDetailsAsync() + { + if (_areMotherboardDetailsLoaded || _areMotherboardDetailsLoading) + { + return; + } + + _areMotherboardDetailsLoading = true; + MotherboardDetails.Clear(); + MotherboardDetails.Add(new MotherboardDetailDisplay("Motherboard details", "Loading...")); + try + { + IReadOnlyList details = await Task.Run(SystemSpecsService.GetMotherboardDetails); + MotherboardDetails.Clear(); + if (details.Count == 0) + { + MotherboardDetails.Add(new MotherboardDetailDisplay( + "Motherboard details", + "Detailed firmware information is unavailable on this system.")); + return; + } + + foreach (MotherboardDetail detail in details) + { + MotherboardDetails.Add(new MotherboardDetailDisplay(detail.Label, detail.Value)); + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] Motherboard details were unavailable."); + MotherboardDetails.Clear(); + MotherboardDetails.Add(new MotherboardDetailDisplay( + "Motherboard details", + "Detailed firmware information is unavailable on this system.")); + } + finally + { + _areMotherboardDetailsLoading = false; + _areMotherboardDetailsLoaded = true; + } + } + public void RefreshCpuLiveMetrics() + { + CpuLiveMetrics metrics = _cpuUsageSampler.Sample(); + if (metrics.UtilizationPercent.HasValue) + { + CpuUtilizationText = $"{metrics.UtilizationPercent.Value}%"; + } + + if (!metrics.AverageFrequencyMHz.HasValue) + { + return; + } + + decimal frequencyMHz = metrics.AverageFrequencyMHz.Value; + CpuCurrentFrequencyText = FormatLiveCpuFrequency(frequencyMHz); + _minimumObservedCpuFrequencyMHz = !_minimumObservedCpuFrequencyMHz.HasValue + ? frequencyMHz + : Math.Min(_minimumObservedCpuFrequencyMHz.Value, frequencyMHz); + _maximumObservedCpuFrequencyMHz = !_maximumObservedCpuFrequencyMHz.HasValue + ? frequencyMHz + : Math.Max(_maximumObservedCpuFrequencyMHz.Value, frequencyMHz); + CpuObservedFrequencyText = _minimumObservedCpuFrequencyMHz == _maximumObservedCpuFrequencyMHz + ? FormatLiveCpuFrequency(_minimumObservedCpuFrequencyMHz.Value) + : $"{FormatLiveCpuFrequency(_minimumObservedCpuFrequencyMHz.Value)} - {FormatLiveCpuFrequency(_maximumObservedCpuFrequencyMHz.Value)}"; + } + + private async Task LoadCpuDetailsAsync(CpuSpec? cpu) + { + try + { + CpuZProcessorDetails? details = await Task.Run(CpuZMemoryReportService.GetCurrentProcessorDetails); + if (details is not null) + { + DisplayCpuDetails(cpu, details); + CpuDetailsText = CreateCpuSummary(cpu, details); + return; + } + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] CPU-Z processor report was unavailable."); + } + + CpuDetailsText = CreateCpuSummary(cpu); + } + + private void DisplayCpuDetails(CpuSpec? cpu, CpuZProcessorDetails? details) + { + CpuDetails.Clear(); + if (cpu is null) + { + return; + } + + AddCpuDetail("Cores", cpu.Cores.ToString(CultureInfo.InvariantCulture)); + AddCpuDetail("Threads", cpu.LogicalProcessors.ToString(CultureInfo.InvariantCulture)); + if (details is null) + { + if (cpu.MaxClockSpeedMHz > 0) + { + AddCpuDetail("Reported maximum frequency", FormatCpuFrequency(cpu.MaxClockSpeedMHz)); + } + + AddCpuDetail("CPU details", "Loading from CPU-Z..."); + return; + } + + if (details.MinimumFrequencyMHz.HasValue && details.MaximumFrequencyMHz.HasValue) + { + AddCpuDetail( + "Minimum - maximum frequency", + $"{FormatCpuFrequency(details.MinimumFrequencyMHz.Value)} - {FormatCpuFrequency(details.MaximumFrequencyMHz.Value)}"); + } + else if (details.MaximumFrequencyMHz.HasValue) + { + AddCpuDetail("Maximum frequency", FormatCpuFrequency(details.MaximumFrequencyMHz.Value)); + } + + AddCoreSet(details.PerformanceCores); + AddCoreSet(details.EfficientCores); + AddCpuDetail("L1 data cache", details.L1DataCache); + AddCpuDetail("L1 instruction cache", details.L1InstructionCache); + AddCpuDetail("L2 cache", details.L2Cache); + AddCpuDetail("L3 cache", details.L3Cache); + if (details.HasAmd3dVCache) + { + AddCpuDetail("AMD 3D V-Cache", "Detected"); + } + + AddCpuDetail("Manufacturer", details.Manufacturer); + AddCpuDetail("Codename", details.Codename); + AddCpuDetail("Socket", details.Socket); + AddCpuDetail("Process", details.Technology); + AddCpuDetail("Thermal design power", details.ThermalDesignPower); + AddCpuDetail("Temperature limit", details.TemperatureLimit); + AddCpuDetail("CPUID", details.Cpuid); + AddCpuDetail("Stepping", details.Stepping); + AddCpuDetail("Instruction sets", details.InstructionSets); + } + + private void AddCoreSet(CpuZCoreSet? coreSet) + { + if (coreSet is null) + { + return; + } + + string value = $"{coreSet.Cores} cores, {coreSet.Threads} threads"; + if (coreSet.MaximumFrequencyMHz.HasValue) + { + value += $" · up to {FormatCpuFrequency(coreSet.MaximumFrequencyMHz.Value)}"; + } + + AddCpuDetail(coreSet.Name, value); + } + + private void AddCpuDetail(string label, string? value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + CpuDetails.Add(new CpuDetailDisplay(label, value)); + } + } + + private static string CreateCpuSummary(CpuSpec? cpu, CpuZProcessorDetails? details = null) + { + if (cpu is null) + { + return string.Empty; + } + + List summary = new() + { + $"{cpu.Cores} cores", + $"{cpu.LogicalProcessors} threads" + }; + if (details?.MinimumFrequencyMHz is decimal minimumFrequencyMHz + && details.MaximumFrequencyMHz is decimal maximumFrequencyMHz) + { + summary.Add($"{FormatCpuFrequency(minimumFrequencyMHz)} - {FormatCpuFrequency(maximumFrequencyMHz)}"); + } + else if (cpu.MaxClockSpeedMHz > 0) + { + summary.Add($"up to {FormatCpuFrequency(cpu.MaxClockSpeedMHz)}"); + } + + return string.Join(" · ", summary); + } + + private static string FormatCpuFrequency(decimal megahertz) => megahertz >= 1000m + ? (megahertz / 1000m).ToString("0.##", CultureInfo.InvariantCulture) + " GHz" + : megahertz.ToString("0", CultureInfo.InvariantCulture) + " MHz"; + + private static string FormatLiveCpuFrequency(decimal megahertz) => + megahertz.ToString("0", CultureInfo.InvariantCulture) + " MHz"; + + private static string FormatCpuFrequency(uint megahertz) => FormatCpuFrequency((decimal)megahertz); + private async Task LoadMemoryTimingDetailsAsync(IReadOnlyList modules) + { + try + { + IReadOnlyList modulesWithTimings = await Task.Run( + () => SystemSpecsService.AddCurrentMemoryTimingDetails(modules)); + DisplayMemoryModules(modulesWithTimings); + } + catch (Exception exception) + { + App.logger.Debug(exception, "[Specs] CPU-Z memory timing report was unavailable."); + } + finally + { + MemoryDescriptionText = TotalMemoryText; + } + } + + private void DisplayMemoryModules(IReadOnlyList modules) + { + MemoryModules.Clear(); + foreach (MemoryModuleSpec module in modules) + { + string manufacturer = string.IsNullOrWhiteSpace(module.Manufacturer) ? "Unknown manufacturer" : module.Manufacturer!; + string header = string.IsNullOrWhiteSpace(module.SlotLabel) + ? manufacturer + : $"{module.SlotLabel} · {manufacturer}"; + List details = new() { FormatBytes(module.CapacityBytes) }; + if (!string.IsNullOrWhiteSpace(module.MemoryType)) + { + details.Add(module.MemoryType!); + } + if (module.SpeedMHz.HasValue) + { + details.Add($"{module.SpeedMHz.Value:N0} MT/s"); + } + if (!string.IsNullOrWhiteSpace(module.TimingText)) + { + details.Add(module.TimingText!); + } + MemoryModules.Add(new MemoryModuleDisplay(header, string.Join(" · ", details))); + } + } private static string FormatNetworkSpeed(ulong bitsPerSecond) { const double gigabit = 1_000_000_000d; diff --git a/SynToolkit/Views/AdjustmentsPage.xaml b/SynToolkit/Views/AdjustmentsPage.xaml index d51a151..223f669 100644 --- a/SynToolkit/Views/AdjustmentsPage.xaml +++ b/SynToolkit/Views/AdjustmentsPage.xaml @@ -119,7 +119,7 @@ + Text="Add wallpaper" local:LocalizedText.TextKey="AdjustmentsPageTextB573A9CEB952" /> @@ -132,11 +132,11 @@ + Text="Customizations" local:LocalizedText.TextKey="AdjustmentsPageText5732BA5E3133" /> + Title="Administrator access required" local:LocalizedText.TitleKey="AdjustmentsPageTitleA526402028D0" /> - + - - - + @@ -196,33 +196,33 @@ - - - - - + -