From c5c5ee1f3679b5fc883bcc1190d12f7a47f34b46 Mon Sep 17 00:00:00 2001
From: realdody <65219829+realdody@users.noreply.github.com>
Date: Tue, 14 Jul 2026 11:24:05 +0200
Subject: [PATCH] [logging] Fetch hardware info, add to video window title and
log (#117)
---
src/SharpEmu.CLI/Program.cs | 1 +
src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 4 +-
src/SharpEmu.Logging/HostSystemInfo.cs | 178 ++++++++++++++++++
3 files changed, 182 insertions(+), 1 deletion(-)
create mode 100644 src/SharpEmu.Logging/HostSystemInfo.cs
diff --git a/src/SharpEmu.CLI/Program.cs b/src/SharpEmu.CLI/Program.cs
index caf710d..39b3221 100644
--- a/src/SharpEmu.CLI/Program.cs
+++ b/src/SharpEmu.CLI/Program.cs
@@ -95,6 +95,7 @@ internal static partial class Program
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
+ Log.Info(HostSystemInfo.Summary);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
index 3f96f1f..b77505a 100644
--- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
+++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
+using SharpEmu.Logging;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -133,9 +134,10 @@ public static class VideoOutExports
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
+ var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName} \u00b7 {HostSystemInfo.GpuName}";
lock (_stateGate)
{
- _windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
+ _windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
}
}
diff --git a/src/SharpEmu.Logging/HostSystemInfo.cs b/src/SharpEmu.Logging/HostSystemInfo.cs
new file mode 100644
index 0000000..e1493a6
--- /dev/null
+++ b/src/SharpEmu.Logging/HostSystemInfo.cs
@@ -0,0 +1,178 @@
+// Copyright (C) 2026 SharpEmu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+using Microsoft.Win32;
+using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+
+namespace SharpEmu.Logging;
+
+/// Provides best-effort information about the host system.
+public static class HostSystemInfo
+{
+ private static readonly Lazy CpuNameValue = new(GetCpuName);
+ private static readonly Lazy GpuNameValue = new(GetPreferredGpuName);
+ private static readonly Lazy MemoryDescriptionValue = new(GetMemoryDescription);
+
+ /// Host CPU name, or a safe fallback when it cannot be determined.
+ public static string CpuName => CpuNameValue.Value;
+
+ /// Preferred physical GPU name, or a safe fallback when it cannot be determined.
+ public static string GpuName => GpuNameValue.Value;
+
+ /// Returns a concise description of the host hardware for diagnostic logs.
+ public static string Summary =>
+ $"Host hardware: CPU: {CpuName}; GPU: {GpuName}; RAM: {MemoryDescriptionValue.Value}.";
+
+ private static string GetCpuName()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return $"{Environment.ProcessorCount} logical processors";
+ }
+
+ try
+ {
+ using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
+ if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
+ {
+ return Regex.Replace(
+ name.Trim(),
+ @"\s+\d+-Core Processor$",
+ string.Empty,
+ RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
+ }
+ }
+ catch (Exception)
+ {
+ // Hardware information is diagnostic only.
+ }
+
+ return $"{Environment.ProcessorCount} logical processors";
+ }
+
+ private static string GetPreferredGpuName()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return "unknown";
+ }
+
+ try
+ {
+ var preferredName = "unknown";
+ var preferredScore = int.MinValue;
+ for (uint index = 0; ; index++)
+ {
+ var device = new DisplayDevice
+ {
+ cb = Marshal.SizeOf(),
+ };
+
+ if (!EnumDisplayDevices(null, index, ref device, 0))
+ {
+ break;
+ }
+
+ var name = device.DeviceString?.Trim();
+ var score = ScoreGpu(name);
+ if (score > preferredScore)
+ {
+ preferredName = name!;
+ preferredScore = score;
+ }
+ }
+
+ return preferredScore > 0 ? preferredName : "unknown";
+ }
+ catch (Exception)
+ {
+ // Hardware information is diagnostic only.
+ return "unknown";
+ }
+ }
+
+ private static int ScoreGpu(string? name)
+ {
+ if (string.IsNullOrWhiteSpace(name) ||
+ name.Contains("virtual", StringComparison.OrdinalIgnoreCase) ||
+ name.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
+ name.Contains("basic display", StringComparison.OrdinalIgnoreCase))
+ {
+ return -1;
+ }
+
+ if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
+ name.Contains("geforce", StringComparison.OrdinalIgnoreCase) ||
+ name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
+ name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
+ {
+ return 2;
+ }
+
+ return 1;
+ }
+
+ private static string GetMemoryDescription()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ try
+ {
+ var status = new MemoryStatusEx
+ {
+ dwLength = (uint)Marshal.SizeOf(),
+ };
+ if (GlobalMemoryStatusEx(ref status))
+ {
+ var megabytes = status.ullTotalPhys / (1024 * 1024);
+ var gigabytes = status.ullTotalPhys / (1024d * 1024 * 1024);
+ return $"{megabytes:N0} MB ({gigabytes:N1} GB)";
+ }
+ }
+ catch (Exception)
+ {
+ // Hardware information is diagnostic only.
+ }
+ }
+
+ return "unknown";
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct DisplayDevice
+ {
+ public int cb;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
+ public string? DeviceName;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string? DeviceString;
+ public int StateFlags;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string? DeviceId;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string? DeviceKey;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+ private struct MemoryStatusEx
+ {
+ public uint dwLength;
+ public uint dwMemoryLoad;
+ public ulong ullTotalPhys;
+ public ulong ullAvailPhys;
+ public ulong ullTotalPageFile;
+ public ulong ullAvailPageFile;
+ public ulong ullTotalVirtual;
+ public ulong ullAvailVirtual;
+ public ulong ullAvailExtendedVirtual;
+ }
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool EnumDisplayDevices(string? deviceName, uint deviceNum, ref DisplayDevice displayDevice, uint flags);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
+}