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; /// /// GPU vendor, used to try the hardware encoders of the active GPU first (VideoEncoder.Vendor). Unknown is /// also the vendor of the software encoders. /// [SuppressMessage("ReSharper", "InconsistentNaming")] internal enum GPUVendor { Unknown, NVIDIA, AMD, Intel, Apple } /// /// 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). /// 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 " 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 " or "Intel(R) Arc(TM) 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 " with an optional " XT". // ReSharper disable once InconsistentNaming private const string AMDPattern = @"^AMD Radeon RX [0-9]+(?> XT)?$"; /// Vendor found by the first GetActiveGPUVendor call. private static GPUVendor _cachedVendor = GPUVendor.Unknown; /// True once _cachedVendor is set. private static bool _hasCachedVendor; /// /// 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. /// private static GPUVendor GetActiveGPUVendor() { if (_hasCachedVendor) return _cachedVendor; var gpuName = SystemInfo.graphicsDeviceName; Melon.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; } /// Result of the first GetNvidiaDriverVersion call (null when not available). private static double? _cachedNvidiaVersion; /// True once _cachedNvidiaVersion is set. private static bool _hasCachedNvidiaVersion; /// /// 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. /// /// 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. /// /// 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.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.Logger.Msg($"Parsed NVIDIA Driver Version from Registry (RegistryUtil): {_cachedNvidiaVersion:F2}"); return _cachedNvidiaVersion; } return null; } /// /// Encoders for 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). /// /// Video codec. /// When true, only the software encoders of the codec are returned. public static List 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(); var others = new List(); foreach (var item in encoders) { if (item.Vendor == vendor) prioritized.Add(item); else others.Add(item); } prioritized.AddRange(others); return prioritized; } }