Files
ADOFAIRenderer/Util/GPUUtil.cs
T
2026-10-15 16:17:13 +09:00

196 lines
8.4 KiB
C#

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using ADOFAIRenderer.Data;
using ADOFAIRenderer.Util.FFmpeg;
using ADOFAIRenderer.Util.OS;
using MelonLoader;
using UnityEngine;
namespace ADOFAIRenderer.Util;
/// <summary>
/// GPU vendor, used to try the hardware encoders of the active GPU first (VideoEncoder.Vendor). <c>Unknown</c> is
/// also the vendor of the software encoders.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming")]
internal enum GPUVendor
{
Unknown, NVIDIA, AMD, Intel, Apple
}
/// <summary>
/// Detection of the active GPU vendor and the NVIDIA driver version, and the encoder try order derived from the vendor.
/// Results are cached for the game session. Main thread (SystemInfo is read on first use).
/// </summary>
internal static class GPUUtil
{
// Device name patterns (SystemInfo.graphicsDeviceName) of the GPU families recognized per vendor. A name that matches
// none of them gives GPUVendor.Unknown (Apple on macOS), which only affects the encoder try order.
// NVIDIA: "NVIDIA GeForce RTX/GTX <number>" with optional SUPER / Ti / Ti SUPER, memory size, "Laptop GPU" and a
// "/PCIe/SSE2" suffix.
private const string NvidiaPattern =
@"^NVIDIA GeForce [RG]TX [0-9]+(?> SUPER| Ti(?> SUPER)?)?(?> [0-9]+GB)?(?> Laptop GPU)?(?>/PCIe/SSE2)?$";
// Intel: "Intel(R) UHD/HD Graphics <number>" or "Intel(R) Arc(TM) <A-C><number> Graphics", with an optional
// parenthesized suffix.
private const string IntelPattern =
@"^Intel(?>\(R\))? (?>U?HD Graphics [0-9]+|Arc\(TM\) [A-C][0-9]+ Graphics)(?> \([^)]+\))?$";
// AMD: "AMD Radeon RX <number>" with an optional " XT".
// ReSharper disable once InconsistentNaming
private const string AMDPattern =
@"^AMD Radeon RX [0-9]+(?> XT)?$";
/// <summary>Vendor found by the first GetActiveGPUVendor call.</summary>
private static GPUVendor _cachedVendor = GPUVendor.Unknown;
/// <summary>True once _cachedVendor is set.</summary>
private static bool _hasCachedVendor;
/// <summary>
/// Vendor of the GPU Unity renders with: the device name is matched against the NVIDIA, Intel and AMD patterns in
/// that order; on macOS an unmatched name gives Apple; otherwise Unknown. Logs the device name once.
/// </summary>
private static GPUVendor GetActiveGPUVendor()
{
if (_hasCachedVendor) return _cachedVendor;
var gpuName = SystemInfo.graphicsDeviceName;
Melon<RenderMod>.Logger.Msg($"Active GPU: {gpuName}");
_cachedVendor = GPUVendor.Unknown;
foreach (Match unused in Regex.Matches(gpuName, NvidiaPattern))
{
_cachedVendor = GPUVendor.NVIDIA;
break;
}
if (_cachedVendor == GPUVendor.Unknown)
{
foreach (Match unused in Regex.Matches(gpuName, IntelPattern))
{
_cachedVendor = GPUVendor.Intel;
break;
}
}
if (_cachedVendor == GPUVendor.Unknown)
{
foreach (Match unused in Regex.Matches(gpuName, AMDPattern))
{
_cachedVendor = GPUVendor.AMD;
break;
}
}
if (_cachedVendor == GPUVendor.Unknown && Application.platform == RuntimePlatform.OSXPlayer)
{
_cachedVendor = GPUVendor.Apple;
}
_hasCachedVendor = true;
return _cachedVendor;
}
/// <summary>Result of the first GetNvidiaDriverVersion call (null when not available).</summary>
private static double? _cachedNvidiaVersion;
/// <summary>True once _cachedNvidiaVersion is set.</summary>
private static bool _hasCachedNvidiaVersion;
/// <summary>
/// NVIDIA driver version in NVIDIA's numbering (for example 561.09), or null when the active GPU is not detected as
/// NVIDIA or no version can be found. The result is cached.
/// <para>
/// The version is derived from a four-part Windows driver version a.b.c.d as ((c mod 10) x 10000 + d) / 100,
/// for example 31.0.15.6109 gives 561.09. It is read from SystemInfo.graphicsDeviceVersion when that string contains a
/// four-part number, otherwise (Windows only) from the DriverVersion value of the first display adapter entry whose
/// ProviderName contains "NVIDIA" under the display adapter class key of the registry.
/// </para>
/// </summary>
public static double? GetNvidiaDriverVersion()
{
if (_hasCachedNvidiaVersion) return _cachedNvidiaVersion;
_hasCachedNvidiaVersion = true;
_cachedNvidiaVersion = null;
if (GetActiveGPUVendor() != GPUVendor.NVIDIA) return null;
// 1. Try parsing from UnityEngine.SystemInfo.graphicsDeviceVersion
var deviceVersion = SystemInfo.graphicsDeviceVersion;
if (!string.IsNullOrEmpty(deviceVersion))
{
var match = Regex.Match(deviceVersion, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b");
if (match.Success)
{
if (int.TryParse(match.Groups[3].Value, out var sub) && int.TryParse(match.Groups[4].Value, out int build))
{
var nvidiaVerInt = sub % 10 * 10000 + build;
_cachedNvidiaVersion = nvidiaVerInt / 100.0;
Melon<RenderMod>.Logger.Msg($"Parsed NVIDIA Driver Version from SystemInfo: {_cachedNvidiaVersion:F2}");
return _cachedNvidiaVersion;
}
}
}
// 2. Fallback to Windows Registry (only on Windows NT platforms via RegistryUtil)
if (System.Environment.OSVersion.Platform != System.PlatformID.Win32NT) return null;
// Device class key of display adapters (class GUID {4d36e968-e325-11ce-bfc1-08002be10318}); each numbered subkey is
// one installed adapter driver.
// Reference: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors
const string classKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
var subkeys = RegistryUtil.GetHKLMSubKeyNames(classKey);
if (subkeys == null) return null;
foreach (var subkey in subkeys)
{
var subkeyPath = classKey + "\\" + subkey;
var provider = RegistryUtil.GetHKLMValue(subkeyPath, "ProviderName");
if (provider == null || !provider.Contains("NVIDIA")) continue;
var driverVersionStr = RegistryUtil.GetHKLMValue(subkeyPath, "DriverVersion");
if (string.IsNullOrEmpty(driverVersionStr)) continue;
var regMatch = Regex.Match(driverVersionStr, @"\b(\d+)\.(\d+)\.(\d+)\.(\d+)\b");
if (!regMatch.Success) continue;
if (!int.TryParse(regMatch.Groups[3].Value, out var sub) ||
!int.TryParse(regMatch.Groups[4].Value, out var build)) continue;
var nvidiaVerInt = sub % 10 * 10000 + build;
_cachedNvidiaVersion = nvidiaVerInt / 100.0;
Melon<RenderMod>.Logger.Msg($"Parsed NVIDIA Driver Version from Registry (RegistryUtil): {_cachedNvidiaVersion:F2}");
return _cachedNvidiaVersion;
}
return null;
}
/// <summary>
/// Encoders for <paramref name="codec"/> in the order they are tried: the encoders of the active GPU vendor first,
/// then all others in VideoEncoder.All order (hardware encoders of other vendors and the software encoders).
/// </summary>
/// <param name="codec">Video codec.</param>
/// <param name="forceSoftware">When true, only the software encoders of the codec are returned.</param>
public static List<VideoEncoder> GetPrioritizedEncoders(Codec codec, bool forceSoftware = false)
{
var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList();
if (forceSoftware)
{
return encoders.Where(e => !e.IsHardware).ToList();
}
var vendor = GetActiveGPUVendor();
var prioritized = new List<VideoEncoder>();
var others = new List<VideoEncoder>();
foreach (var item in encoders)
{
if (item.Vendor == vendor) prioritized.Add(item);
else others.Add(item);
}
prioritized.AddRange(others);
return prioritized;
}
}