Введение
Всем привет! Сегодня я расскажу о своем пути к написанию демона для работы с видеокартами (пока только от Nvidia).
Для начала немного предыстории о себе. Для меня Linux — не новинка, но и не та ОС, с которой я долго взаимодействовал в качестве десктопа. Я разработчик с коммерческим стажем в финтехе, и с Linux ранее был знаком только как с очень практичной серверной ОС. Но однажды я стал полноценным «линуксоидом», решив перейти к более гибкой в настройке и кастомизации системе. Я выбрал дистрибутив на основе Arch, а именно CachyOS. И, как и многие, при наличии свободного времени я люблю поиграть в игры.
Моя боль как любителя поиграть
Я столкнулся с проблемой: мне нередко приходится настраивать переменные окружения через Goverlay, Nvidia X Server (nvidia‑smi) и пихать это все в команды запуска в свойствах Steam. Либо использовать сторонние лаунчеры по типу Heroic или Lutris и делать схожие настройки там. Как бывшему пользователю Windows, мне захотелось как‑то упростить для себя процесс настройки видеокарты, запуска игр и подбора оптимальных параметров для Linux‑гейминга.
Мне известны аппки по типу GreenWithEnvy, LACT или CoreCtrl. Но с переходом на Wayland могут всплыть нюансы вроде жестких привязок к X11. Я хотел иметь под рукой что‑то вроде MSI Afterburner или даже Nvidia App на Windows, чтобы можно было настраивать такие параметры, как:
Какую модель DLSS использовать.
Настройки на уровне драйвера без танцев с бубном
Применение некоторых возможностей на уровне драйвера вне зависимости от наличия поддержки функционала в самой игре.
Решение проблем с простоями, а именно — излишнего потребления питания и работы кулеров без нагрузки (на Reddit под моим постом писали о кейсах с Intel, когда даже в простое вентиляторы могли постоянно включаться и выключаться).
Также хотелось, отчасти как и в LACT, сделать поддержку нескольких брендов (замечал, что там были Nvidia и Intel). Я хочу объединить в одном приложении все три бренда. Но с учетом того, что библиотеки Nvidia сразу имеют наборы методов для работы с картами не только на обычных юзерских ПК, но и в крупных дата‑центрах, со временем я хочу попробовать сделать что‑то ультимативное.
Архитектура и выбор стека
Не буду скрывать: при написании применялся ИИ для ускорения импорта методов из библиотеки «зеленых» по шаблонам, которые я написал сам. Изначально либка написана под C++, а я пишу на C#. Также я применяю ИИ во фронтенде, в котором не силен, потому что привык работать с бэкендом и не имею достаточно навыков для написания красивого UI. При этом некоторые вещи так или иначе делаю сам. И да, поначалу мой репозиторий в Git был откровенно мусорным — имея основную работу и семью, не всегда находишь силы и время на то, чтобы всё вычистить и аккуратно выложить =).
Если вы видели пост на Reddit об обнове, то могли заметить и гневные негативные комменты о проекте и применении нейросетей в процессе написания кода. Заранее скажу: хейтеры ИИ‑инструментов, я люблю и ценю вас, ведь благодаря вам посты чаще уходят в массы! Вайб‑кодинг — это неплохо (если подходить с умом), но ИИ не должен полностью заменять человека. Нельзя деградировать и опускаться до тупейшего скармливания небольших инструкций и вечных правок кода, написанного через один ИИ с помощью другого.
Итак, возвращаясь непосредственно к самому пет‑проекту и его технической части: ниже скриншот того, как выглядит UI сейчас.

В данном проекте я решил сделать UI с помощью Vue.js. План был простым: запускать аппку и внутри отрисовывать HTML с применением Photino.NET. Ниже — пример того, как это будет преобразовываться из веб‑версии (HTML, CSS, Vue.js) в нативные окна. Окна создаются через GTK3/GTK4, которые отлично работают в Wayland. Photino встраивает в окно очень легкий движок WebKitGTK и загружает туда статичные файлы Vue (понятное дело, сбилженные в маленькую папку типа dist с index.html и парой JS‑файлов).
using PhotinoNET; class Program { [STAThread] static void Main(string[] args) { // Создаем нативное окно var window = new PhotinoWindow() .SetTitle("UnixGPUForge") .SetSize(800, 600) // Загружаем наши скомпилированные Vue-файлы .Load("ui/index.html"); window.WaitForClose(); } }
Любой, кто откроет проект, спросит, почему я выбрал.NET. Отвечу: C#.NET компилируется в нативный AOT‑бинарник, потребление памяти минимально, плюс очень удобно асинхронно работать с потоками I/O (что критично при работе с датчиками температуры, потребления и так далее).
Для работы с видеокартами и с заделом на будущее я строил архитектуру абстрактно, так как планирую поддержку карт не только от Nvidia, но и от AMD и Intel. Я накидал интерфейс IGpuProvider, в котором нужно реализовывать базовые методы получения телеметрии и стейтов.
Как я писал взаимодействие с NVML
Самое главное при работе с Nvidia у меня происходит в провайдере NvidiaGpuProvider. В нем идет импорт NVML — так я работаю с картой напрямую и избавляюсь от необходимости парсить выводы консоли nvidia-smi.
Ниже выложен полный код провайдера (последней версии на момент написания статьи).
using System.Runtime.InteropServices; using UnixGPUForge.Daemon.Core.Interfaces; using UnixGPUForge.Shared.Models; namespace UnixGPUForge.Daemon.Core.Providers; public partial class NvidiaGpuProvider : IGpuProvider, IDisposable { private const string NvmlLibrary = "libnvidia-ml.so"; private readonly IntPtr _deviceHandle; [StructLayout(LayoutKind.Sequential)] private struct nvmlUtilization_t { public uint gpu; public uint memory; } [StructLayout(LayoutKind.Sequential)] private struct nvmlMemory_t { public ulong total; public ulong free; public ulong used; } [StructLayout(LayoutKind.Sequential)] private struct nvmlProcessInfo_t { public uint pid; public ulong usedGpuMemory; public uint gpuInstanceId; public uint computeInstanceId; } [LibraryImport(NvmlLibrary, EntryPoint = "nvmlInit_v2")] private static partial int NvmlInit(); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlShutdown")] private static partial int NvmlShutdown(); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetHandleByIndex_v2")] private static partial int NvmlDeviceGetHandleByIndex(uint index, out IntPtr device); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetName", StringMarshalling = StringMarshalling.Utf8)] private static partial int NvmlDeviceGetName(IntPtr device, IntPtr name, uint length); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetTemperature")] private static partial int NvmlDeviceGetTemperature(IntPtr device, uint sensorType, out uint temp); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetPowerUsage")] private static partial int NvmlDeviceGetPowerUsage(IntPtr device, out uint power); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetUtilizationRates")] private static partial int NvmlDeviceGetUtilizationRates(IntPtr device, out nvmlUtilization_t utilization); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetMemoryInfo")] private static partial int NvmlDeviceGetMemoryInfo(IntPtr device, out nvmlMemory_t memory); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetPowerManagementLimitConstraints")] private static partial int NvmlDeviceGetPowerManagementLimitConstraints(IntPtr device, out uint minLimit, out uint maxLimit); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceSetPowerManagementLimit_v2")] private static partial int NvmlDeviceSetPowerManagementLimit(IntPtr device, uint limit); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceSetGpuLockedClocks")] private static partial int NvmlDeviceSetGpuLockedClocks(IntPtr device, uint minGpuClockMHz, uint maxGpuClockMHz); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceResetGpuLockedClocks")] private static partial int NvmlDeviceResetGpuLockedClocks(IntPtr device); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceSetMemoryLockedClocks")] private static partial int NvmlDeviceSetMemoryLockedClocks(IntPtr device, uint minMemClockMHz, uint maxMemClockMHz); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceResetMemoryLockedClocks")] private static partial int NvmlDeviceResetMemoryLockedClocks(IntPtr device); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetGraphicsRunningProcesses_v3")] private static partial int NvmlDeviceGetGraphicsRunningProcesses(IntPtr device, ref uint infoCount, [Out] nvmlProcessInfo_t[] infos); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetComputeRunningProcesses_v3")] private static partial int NvmlDeviceGetComputeRunningProcesses(IntPtr device, ref uint infoCount, [Out] nvmlProcessInfo_t[] infos); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceSetFanSpeed_v2")] private static partial int NvmlDeviceSetFanSpeed(IntPtr device, uint fan, uint speed); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceSetDefaultFanSpeed_v2")] private static partial int NvmlDeviceSetDefaultFanSpeed(IntPtr device, uint fan); [LibraryImport(NvmlLibrary, EntryPoint = "nvmlDeviceGetNumFans")] private static partial int NvmlDeviceGetNumFans(IntPtr device, out uint numFans); public NvidiaGpuProvider() { if (NvmlInit() != 0) throw new Exception("Не удалось инициализировать NVML драйвер."); if (NvmlDeviceGetHandleByIndex(0, out _deviceHandle) != 0) throw new Exception("Видеокарта NVIDIA не найдена."); } public GpuTelemetry GetTelemetry() { IntPtr namePtr = Marshal.AllocHGlobal(96); string gpuName = "Unknown GPU"; if (NvmlDeviceGetName(_deviceHandle, namePtr, 96) == 0) { gpuName = Marshal.PtrToStringUTF8(namePtr) ?? gpuName; } Marshal.FreeHGlobal(namePtr); NvmlDeviceGetTemperature(_deviceHandle, 0, out uint temp); NvmlDeviceGetPowerUsage(_deviceHandle, out uint powerMw); NvmlDeviceGetUtilizationRates(_deviceHandle, out nvmlUtilization_t util); NvmlDeviceGetMemoryInfo(_deviceHandle, out nvmlMemory_t mem); NvmlDeviceGetPowerManagementLimitConstraints(_deviceHandle, out uint minLimitMw, out uint maxLimitMw); return new GpuTelemetry { Name = gpuName, Temperature = temp, PowerUsage = powerMw / 1000, CoreLoad = util.gpu, VramUsed = mem.used / (1024 * 1024), VramTotal = mem.total / (1024 * 1024), MinLimit = minLimitMw / 1000, MaxLimit = maxLimitMw / 1000 }; } public bool SetPowerLimit(uint watts) { return NvmlDeviceSetPowerManagementLimit(_deviceHandle, watts * 1000) == 0; } public bool SetGpuLockedClocks(uint minMHz, uint maxMHz) { return NvmlDeviceSetGpuLockedClocks(_deviceHandle, minMHz, maxMHz) == 0; } public bool ResetGpuLockedClocks() { return NvmlDeviceResetGpuLockedClocks(_deviceHandle) == 0; } public bool SetMemoryLockedClocks(uint minMHz, uint maxMHz) { return NvmlDeviceSetMemoryLockedClocks(_deviceHandle, minMHz, maxMHz) == 0; } public bool ResetMemoryLockedClocks() { return NvmlDeviceResetMemoryLockedClocks(_deviceHandle) == 0; } public bool SetFanSpeed(uint fanIndex, uint speedPercent) { int result = NvmlDeviceSetFanSpeed(_deviceHandle, fanIndex, speedPercent); if (result != 0) { Console.WriteLine($"[NVML] Ошибка управления винтом {fanIndex}. Код ошибки NVML: {result}"); return false; } return true; } public bool ResetFanSpeed(uint fanIndex) { return NvmlDeviceSetDefaultFanSpeed(_deviceHandle, fanIndex) == 0; } public uint GetFanCount() { if (NvmlDeviceGetNumFans(_deviceHandle, out uint fanCount) == 0) { return fanCount; } return 2; } public List<uint> GetRunningPids() { var pids = new HashSet<uint>(); var dummy = Array.Empty<nvmlProcessInfo_t>(); uint graphicsCount = 0; int resGraphics = NvmlDeviceGetGraphicsRunningProcesses(_deviceHandle, ref graphicsCount, dummy); if (resGraphics == 7 && graphicsCount > 0) { var graphicsInfos = new nvmlProcessInfo_t[graphicsCount]; if (NvmlDeviceGetGraphicsRunningProcesses(_deviceHandle, ref graphicsCount, graphicsInfos) == 0) { foreach (var info in graphicsInfos) pids.Add(info.pid); } } uint computeCount = 0; int resCompute = NvmlDeviceGetComputeRunningProcesses(_deviceHandle, ref computeCount, dummy); if (resCompute == 7 && computeCount > 0) { var computeInfos = new nvmlProcessInfo_t[computeCount]; if (NvmlDeviceGetComputeRunningProcesses(_deviceHandle, ref computeCount, computeInfos) == 0) { foreach (var info in computeInfos) pids.Add(info.pid); } } return pids.ToList(); } public void Dispose() { NvmlShutdown(); GC.SuppressFinalize(this); } }
Есть один нюанс при работе с кулерами. Иногда проприетарный драйвер Nvidia может не отдать управление вентиляторами. Для перехвата контроля надо передавать параметры Coolbits в конфиг X‑сервера, но в Wayland это штатно не работает (либо работает кривовато — до конца пока не разобрался). Окончательного решения я еще не принял, но вижу такие варианты: делать внутри демона прямое взаимодействие с интерфейсом I2C видеокарты или использовать низкоуровневые обертки над NVML с API через P/Invoke, чтобы форсировать PWM (ШИМ‑сигнал) вентиляторов в обход дисплейного сервера.
Попытки сделать профилирование
Также я пробую реализовать автопрофилирование для игр. В будущем хочу научиться инжектить переменные в рантайм автоматически, чтобы не прописывать их руками (как я сейчас делаю через Goverlay, формируя конфиг и вставляя его в Steam как команду запуска).
Но это планы. Сейчас профайлер помогает не выставлять вручную настройки карты для управления лимитами питания и частотами ядра/памяти, сохраняя конфиг в JSON.
Код профайлера
using System.Diagnostics; using System.Text.Json; using UnixGPUForge.Daemon.Core.Interfaces; using UnixGPUForge.Shared.Models; namespace UnixGPUForge.Daemon.Core.Services; public class AutoProfileService : BackgroundService { private readonly IGpuProvider _gpu; private readonly ILogger<AutoProfileService> _logger; private List<GameProfile> _profiles = new(); private string? _activeProfileName = null; private static readonly object _nvmlLock = new(); public AutoProfileService(IGpuProvider gpu, ILogger<AutoProfileService> logger) { _gpu = gpu; _logger = logger; LoadProfiles(); } private void LoadProfiles() { try { if (File.Exists("profiles.json")) { string json = File.ReadAllText("profiles.json"); _profiles = JsonSerializer.Deserialize<List<GameProfile>>(json) ?? new List<GameProfile>(); _logger.LogInformation($"Загружено {_profiles.Count} профилей автоматизации."); } else { // Генерируем дефолтный шаблон _profiles = new List<GameProfile> { new() { ProcessName = "Cyberpunk2077", PowerLimit = 300, CoreClock = 2650, MemoryClock = 11200 }, new() { ProcessName = "dota2", PowerLimit = 150, CoreClock = 1800, MemoryClock = 10000 }, new() { ProcessName = "HuntGame", PowerLimit = 280, CoreClock = 2500, MemoryClock = 11200 }, new() { ProcessName = "bg3", PowerLimit = 200, CoreClock = 2000, MemoryClock = 10500 } }; File.WriteAllText("profiles.json", JsonSerializer.Serialize(_profiles, new JsonSerializerOptions { WriteIndented = true })); } } catch (Exception ex) { _logger.LogError($"Ошибка загрузки profiles.json: {ex.Message}"); } } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { LoadProfiles(); var pids = _gpu.GetRunningPids(); GameProfile? matchedProfile = null; foreach (var pid in pids) { try { var process = Process.GetProcessById((int)pid); string procName = process.ProcessName.Replace(".exe", "", StringComparison.OrdinalIgnoreCase); matchedProfile = _profiles.FirstOrDefault(p => procName.Contains(p.ProcessName.Replace(".exe", ""), StringComparison.OrdinalIgnoreCase)); if (matchedProfile != null) break; } catch (Exception ex) { _logger.LogError($"Ошибка получения информации о процессе с PID {pid}: {ex.Message}"); } } if (matchedProfile != null && _activeProfileName != matchedProfile.ProcessName) { _logger.LogInformation($"[AUTO] Обнаружена игра: {matchedProfile.ProcessName}. Применяем настройки железа..."); lock (_nvmlLock) { _gpu.SetPowerLimit(matchedProfile.PowerLimit); _gpu.SetGpuLockedClocks(200, matchedProfile.CoreClock); _gpu.SetMemoryLockedClocks(400, matchedProfile.MemoryClock); } _activeProfileName = matchedProfile.ProcessName; } else if (matchedProfile == null && _activeProfileName != null) { _logger.LogInformation("[AUTO] Игра закрыта. Возврат к дефолтным частотам..."); _gpu.ResetGpuLockedClocks(); _gpu.ResetMemoryLockedClocks(); _activeProfileName = null; } } catch (Exception ex) { _logger.LogError($"Ошибка мониторинга процессов: {ex.Message}"); } await Task.Delay(5000, stoppingToken); } } }
Мои заключения
Сейчас я думаю над тем, как сделать все максимально стабильным. Хочется внедрить такие фишки, как выбор предустановок DLSS у Nvidia (что на Windows легко настраивается через Nvidia App), и в целом добавить весь набор настроек, доступный пользователям «окон». Считаю, что это поможет сделать Linux более дружелюбным к играм.
Хотя сейчас с геймингом проблем всё меньше, многие переходят на Linux именно за производительностью, и удивляются (по крайней мере, в моем окружении), что не могут настраивать железо так же гибко, как на Windows. Несмотря на море открытого софта в любом направлении, далеко не все пользователи знакомы со специфичными полезными утилитами для Linux.
Ниже оставляю ссылку на GitHub для тех, кому стал интересен проект. Я открыт к критике и советам. Спасибо за выделенное на прочтение статьи время, и прошу не судить строго — это мой первый опыт написания подобных материалов.

