more hardware + driver display improvements

This commit is contained in:
Egor 2025-09-26 03:15:38 -07:00
parent c2287b1d5c
commit 3ef91d1198
6 changed files with 334 additions and 143 deletions

View file

@ -109,15 +109,15 @@ const checkVramWarnings = async (backend: string): Promise<Warning[]> => {
if (gpuMemoryInfo) {
const lowVramThreshold = 8;
const validGpus = gpuMemoryInfo.filter(
(gpu) => typeof gpu.totalMemoryGB === 'number'
(gpu) => gpu.totalMemoryGB !== null && gpu.totalMemoryGB !== ''
);
const lowVramGpus = validGpus.filter(
(gpu) => gpu.totalMemoryGB! < lowVramThreshold
(gpu) => parseFloat(gpu.totalMemoryGB!) < lowVramThreshold
);
if (validGpus.length > 0 && lowVramGpus.length === validGpus.length) {
const memoryDetails = lowVramGpus
.map((gpu) => `${gpu.totalMemoryGB!.toFixed(1)}GB`)
.map((gpu) => `${parseFloat(gpu.totalMemoryGB!).toFixed(1)}GB`)
.join(', ');
warnings.push({

View file

@ -1,7 +1,13 @@
/* eslint-disable no-comments/disallowComments */
import si from 'systeminformation';
import {
cpu as siCpu,
cpuFlags,
graphics as siGraphics,
mem as siMem,
memLayout as siMemLayout,
} from 'systeminformation';
import { safeExecute } from '@/utils/node/logging';
import { getGPUData } from '@/utils/node/gpu';
import { getGPUData, isDiscreteBusAddress } from '@/utils/node/gpu';
import type {
CPUCapabilities,
GPUCapabilities,
@ -12,10 +18,25 @@ import { execa } from 'execa';
import { formatDeviceName } from '@/utils/format';
import { platform } from 'process';
const COMMON_EXEC_OPTIONS = {
timeout: 3000,
reject: false,
};
let cpuCapabilitiesCache: CPUCapabilities | null = null;
let basicGPUInfoCache: BasicGPUInfo | null = null;
let gpuCapabilitiesCache: GPUCapabilities | null = null;
let gpuMemoryInfoCache: GPUMemoryInfo[] | null = null;
let vulkanInfoCache: {
discreteGPUs: {
deviceName: string;
driverInfo?: string;
apiVersion?: string;
hasAMD: boolean;
hasNVIDIA: boolean;
}[];
apiVersion?: string;
} | null = null;
export async function detectCPU() {
if (cpuCapabilitiesCache) {
@ -23,7 +44,7 @@ export async function detectCPU() {
}
const result = await safeExecute(async () => {
const [cpu, flags] = await Promise.all([si.cpu(), si.cpuFlags()]);
const [cpu, flags] = await Promise.all([siCpu(), cpuFlags()]);
const devices: string[] = [];
if (cpu.brand) {
@ -61,37 +82,11 @@ export async function detectGPU() {
}
const result = await safeExecute(async () => {
const graphics = await si.graphics();
let hasAMD = false;
let hasNVIDIA = false;
const gpuInfo: string[] = [];
for (const controller of graphics.controllers) {
// Check vendor for AMD
if (
controller.vendor?.toLowerCase().includes('amd') ||
controller.vendor?.toLowerCase().includes('ati')
) {
hasAMD = true;
}
if (controller.vendor?.toLowerCase().includes('nvidia')) {
hasNVIDIA = true;
}
if (controller.model) {
gpuInfo.push(controller.model);
}
if (platform === 'linux') {
return detectLinuxGPUViaVulkan();
} else {
return detectGPUViaSI();
}
const basicInfo = {
hasAMD,
hasNVIDIA,
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
};
basicGPUInfoCache = basicInfo;
return basicInfo;
}, 'GPU detection failed');
const fallbackGPUInfo = {
@ -104,6 +99,192 @@ export async function detectGPU() {
return basicGPUInfoCache;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
async function getVulkanInfo() {
if (vulkanInfoCache) {
return vulkanInfoCache;
}
try {
const { stdout } = await execa(
'vulkaninfo',
['--summary'],
COMMON_EXEC_OPTIONS
);
const discreteGPUs: {
deviceName: string;
driverInfo?: string;
apiVersion?: string;
hasAMD: boolean;
hasNVIDIA: boolean;
}[] = [];
let globalApiVersion: string | undefined;
if (stdout.trim()) {
const lines = stdout.split('\n');
let foundDiscreteGPU = false;
let currentGPU: (typeof discreteGPUs)[0] | null = null;
for (const line of lines) {
if (
!globalApiVersion &&
line.includes('apiVersion') &&
line.includes('=')
) {
const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
if (match) {
globalApiVersion = match[1];
}
}
if (line.includes('PHYSICAL_DEVICE_TYPE_DISCRETE_GPU')) {
foundDiscreteGPU = true;
currentGPU = {
deviceName: '',
hasAMD: false,
hasNVIDIA: false,
};
} else if (
foundDiscreteGPU &&
currentGPU &&
line.includes('deviceName') &&
line.includes('=')
) {
const parts = line.split('=');
if (parts.length >= 2) {
const name = parts[1]?.trim();
if (name) {
currentGPU.deviceName = name;
currentGPU.hasAMD =
name.toLowerCase().includes('amd') ||
name.toLowerCase().includes('radeon');
currentGPU.hasNVIDIA =
name.toLowerCase().includes('nvidia') ||
name.toLowerCase().includes('geforce') ||
name.toLowerCase().includes('rtx') ||
name.toLowerCase().includes('gtx');
}
}
} else if (
foundDiscreteGPU &&
currentGPU &&
line.includes('driverInfo')
) {
const mesaMatch = line.match(/Mesa\s+(.+)/);
if (mesaMatch) {
currentGPU.driverInfo = `Mesa ${mesaMatch[1].trim()}`;
}
} else if (
foundDiscreteGPU &&
currentGPU &&
line.includes('apiVersion') &&
line.includes('=')
) {
const match = line.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
if (match) {
currentGPU.apiVersion = match[1];
if (!globalApiVersion) {
globalApiVersion = match[1];
}
}
} else if (foundDiscreteGPU && currentGPU && line.includes('GPU')) {
if (currentGPU.deviceName) {
discreteGPUs.push(currentGPU);
}
foundDiscreteGPU = false;
currentGPU = null;
}
}
if (foundDiscreteGPU && currentGPU && currentGPU.deviceName) {
discreteGPUs.push(currentGPU);
}
}
vulkanInfoCache = {
discreteGPUs,
apiVersion: globalApiVersion,
};
return vulkanInfoCache;
} catch {
vulkanInfoCache = {
discreteGPUs: [],
};
return vulkanInfoCache;
}
}
async function detectLinuxGPUViaVulkan() {
try {
const vulkanInfo = await getVulkanInfo();
let hasAMD = false;
let hasNVIDIA = false;
const gpuInfo: string[] = [];
for (const gpu of vulkanInfo.discreteGPUs) {
gpuInfo.push(formatDeviceName(gpu.deviceName));
if (gpu.hasAMD) {
hasAMD = true;
}
if (gpu.hasNVIDIA) {
hasNVIDIA = true;
}
}
return {
hasAMD,
hasNVIDIA,
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
};
} catch {
return {
hasAMD: false,
hasNVIDIA: false,
gpuInfo: ['GPU detection failed'],
};
}
}
async function detectGPUViaSI() {
const graphics = await siGraphics();
let hasAMD = false;
let hasNVIDIA = false;
const gpuInfo: string[] = [];
const discreteControllers = graphics.controllers.filter(
(controller) =>
controller.busAddress && isDiscreteBusAddress(controller.busAddress)
);
for (const controller of discreteControllers) {
if (
controller.vendor?.toLowerCase().includes('amd') ||
controller.vendor?.toLowerCase().includes('ati')
) {
hasAMD = true;
}
if (controller.vendor?.toLowerCase().includes('nvidia')) {
hasNVIDIA = true;
}
if (controller.model) {
gpuInfo.push(controller.model);
}
}
return {
hasAMD,
hasNVIDIA,
gpuInfo: gpuInfo.length > 0 ? gpuInfo : ['No GPU information available'],
};
}
export async function detectGPUCapabilities() {
// WARNING: we're not worrying about the users that update their system
// during runtime and not restart. Should we be though?
@ -125,10 +306,7 @@ export async function detectGPUCapabilities() {
async function detectCUDA() {
try {
const { stdout } = await execa('nvidia-smi', [], {
timeout: 5000,
reject: false,
});
const { stdout } = await execa('nvidia-smi', [], COMMON_EXEC_OPTIONS);
if (stdout.trim()) {
const errorPatterns = [
@ -159,11 +337,13 @@ async function detectCUDA() {
const driverMatch = stdout.match(
/Driver Version:\s*(\d+\.\d+(?:\.\d+)?)/
);
if (driverMatch) {
driverVersion = driverMatch[1];
}
const gpuNameMatch = stdout.match(/\|\s+\d+\s+([^|]+)\s+On\s+\|/g);
if (gpuNameMatch) {
for (const match of gpuNameMatch) {
const name = match
@ -193,10 +373,7 @@ async function detectCUDA() {
export async function detectROCm() {
try {
const rocminfoCommand = platform === 'win32' ? 'hipInfo' : 'rocminfo';
const { stdout } = await execa(rocminfoCommand, [], {
timeout: 5000,
reject: false,
});
const { stdout } = await execa(rocminfoCommand, [], COMMON_EXEC_OPTIONS);
if (stdout.trim()) {
const devices: string[] = [];
@ -258,10 +435,10 @@ export async function detectROCm() {
if (platform === 'linux' || platform === 'darwin') {
try {
const { stdout: amdSmiOutput } = await execa('amd-smi', {
timeout: 3000,
reject: false,
});
const { stdout: amdSmiOutput } = await execa(
'amd-smi',
COMMON_EXEC_OPTIONS
);
if (amdSmiOutput.trim()) {
const match =
@ -275,10 +452,11 @@ export async function detectROCm() {
} catch {}
} else {
try {
const { stdout: hipccOutput } = await execa('hipcc', ['--version'], {
timeout: 3000,
reject: false,
});
const { stdout: hipccOutput } = await execa(
'hipcc',
['--version'],
COMMON_EXEC_OPTIONS
);
if (hipccOutput.trim()) {
const hipVersionMatch = hipccOutput.match(
@ -301,16 +479,24 @@ export async function detectROCm() {
'-Command',
`Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Name -like '*AMD*' -or $_.Name -like '*Radeon*' } | Select-Object -First 1 -ExpandProperty DriverVersion`,
],
{
timeout: 3000,
reject: false,
}
COMMON_EXEC_OPTIONS
);
if (driverOutput.trim()) {
driverVersion = driverOutput.trim();
}
} catch {}
} else if (platform === 'linux') {
try {
const vulkanInfo = await getVulkanInfo();
for (const gpu of vulkanInfo.discreteGPUs) {
if (gpu.driverInfo) {
driverVersion = gpu.driverInfo;
break;
}
}
} catch {}
}
return {
@ -329,48 +515,19 @@ export async function detectROCm() {
async function detectVulkan() {
try {
const { stdout } = await execa('vulkaninfo', ['--summary'], {
timeout: 5000,
reject: false,
});
const vulkanInfo = await getVulkanInfo();
if (stdout.trim()) {
const devices: string[] = [];
const lines = stdout.split('\n');
const devices: string[] = [];
for (const line of lines) {
if (line.includes('deviceName') && line.includes('=')) {
const parts = line.split('=');
if (parts.length >= 2) {
const name = parts[1]?.trim();
if (name) {
devices.push(formatDeviceName(name));
}
}
}
}
let version = 'Unknown';
try {
const apiVersionLine = lines.find(
(line) => line.includes('apiVersion') && line.includes('=')
);
if (apiVersionLine) {
const match = apiVersionLine.match(/=\s*(\d+\.\d+(?:\.\d+)?)/);
if (match) {
version = match[1];
}
}
} catch {}
return {
supported: devices.length > 0,
devices,
version,
};
for (const gpu of vulkanInfo.discreteGPUs) {
devices.push(formatDeviceName(gpu.deviceName));
}
return { supported: false, devices: [], version: 'Unknown' };
return {
supported: devices.length > 0,
devices,
version: vulkanInfo.apiVersion || 'Unknown',
};
} catch {
return { supported: false, devices: [], version: 'Unknown' };
}
@ -430,10 +587,7 @@ function findDeviceNameInClInfo(lines: string[], startIndex: number) {
async function detectCLBlast() {
try {
const { stdout } = await execa('clinfo', [], {
timeout: 3000,
reject: false,
});
const { stdout } = await execa('clinfo', [], COMMON_EXEC_OPTIONS);
if (stdout.trim()) {
const devices = parseClInfoOutput(stdout);
@ -485,7 +639,7 @@ export async function detectGPUMemory() {
}
memoryInfo.push({
totalMemoryGB: vram,
totalMemoryGB: vram?.toFixed(2) || null,
});
}
@ -498,9 +652,9 @@ export async function detectGPUMemory() {
export const detectSystemMemory = async () => {
try {
const [memInfo, memLayout] = await Promise.all([si.mem(), si.memLayout()]);
const [memInfo, memLayout] = await Promise.all([siMem(), siMemLayout()]);
const totalGB = Math.round(memInfo.total / 1024 ** 3);
const totalGB = (memInfo.total / 1024 ** 3).toFixed(2);
let speed: number | undefined;
let type: string | undefined;
@ -532,7 +686,7 @@ export const detectSystemMemory = async () => {
};
} catch {
return {
totalGB: Math.round((await si.mem()).total / 1024 ** 3),
totalGB: ((await siMem()).total / 1024 ** 3).toFixed(2),
};
}
};

View file

@ -1,4 +1,4 @@
import si from 'systeminformation';
import { mem, cpuTemperature, currentLoad } from 'systeminformation';
import { BrowserWindow } from 'electron';
import { platform } from 'process';
import { spawn } from 'child_process';
@ -95,14 +95,14 @@ export function stopMonitoring() {
async function collectAndSendCpuMetrics() {
await tryExecute(async () => {
const cpuData = await si.currentLoad();
const cpuData = await currentLoad();
const metrics: CpuMetrics = {
usage: Math.round(cpuData.currentLoad),
};
if (platform === 'linux') {
try {
const tempData = await si.cpuTemperature();
const tempData = await cpuTemperature();
metrics.temperature =
tempData.main && tempData.main > 0
? Math.round(tempData.main)
@ -118,7 +118,7 @@ async function collectAndSendCpuMetrics() {
async function collectAndSendMemoryMetrics() {
await tryExecute(async () => {
const memData = await si.mem();
const memData = await mem();
const usedBytes = memData.active || memData.used;
const totalBytes = memData.total;
const metrics: MemoryMetrics = {

View file

@ -5,11 +5,11 @@ export interface CPUCapabilities {
}
export interface GPUMemoryInfo {
totalMemoryGB: number | null;
totalMemoryGB: string | null;
}
export interface SystemMemoryInfo {
totalGB: number;
totalGB: string;
speed?: number;
type?: string;
}

View file

@ -60,23 +60,40 @@ async function initializeLinuxGPUCache() {
(parseInt(memTotalData.trim(), 10) || 0) / (1024 * 1024 * 1024)
);
if (memoryTotal > 0) {
let hwmonPath: string | undefined;
if (memoryTotal >= 1) {
let isDiscrete = false;
try {
const hwmonEntries = await readdir(`${devicePath}/hwmon`);
const hwmonEntry = hwmonEntries.find((e) =>
e.startsWith('hwmon')
const busAddress = await readFile(`${devicePath}/uevent`, 'utf8');
const pciMatch = busAddress.match(
/PCI_SLOT_NAME=([0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f])/i
);
if (hwmonEntry) {
hwmonPath = `${devicePath}/hwmon/${hwmonEntry}`;
if (pciMatch) {
const fullAddress = pciMatch[1];
const busAddress = fullAddress.substring(5);
isDiscrete = isDiscreteBusAddress(busAddress);
}
} catch {}
gpus.push({
devicePath,
memoryTotal,
hwmonPath,
});
if (isDiscrete) {
let hwmonPath: string | undefined;
try {
const hwmonEntries = await readdir(`${devicePath}/hwmon`);
const hwmonEntry = hwmonEntries.find((e) =>
e.startsWith('hwmon')
);
if (hwmonEntry) {
hwmonPath = `${devicePath}/hwmon/${hwmonEntry}`;
}
} catch {}
gpus.push({
devicePath,
memoryTotal,
hwmonPath,
});
}
}
} catch {
continue;
@ -153,14 +170,14 @@ async function getWindowsGPUData() {
'powershell',
[
'-Command',
`$activeGpus = Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Status -eq 'OK' } | Select-Object -ExpandProperty Name;
`$activeGpus = Get-CimInstance -ClassName Win32_VideoController | Where-Object { $_.Status -eq 'OK' -and $_.Name -notlike '*Intel*UHD*' -and $_.Name -notlike '*Intel*Iris*' -and $_.Name -notlike '*Basic Display*' } | Select-Object -ExpandProperty Name;
$gpuKeys = Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}' | Where-Object { $_.PSChildName -match '^\\d{4}$' };
foreach($key in $gpuKeys) {
$props = Get-ItemProperty $key.PSPath -ErrorAction SilentlyContinue;
if($props.DriverDesc -and $props.'HardwareInformation.qwMemorySize' -and $activeGpus -contains $props.DriverDesc) {
$vramBytes = $props.'HardwareInformation.qwMemorySize';
$vramGB = [math]::Round($vramBytes/1GB, 2);
if($vramGB -gt 0.5) {
if($vramGB -ge 1) {
Write-Output "$($props.DriverDesc)|$vramGB";
}
}
@ -201,3 +218,26 @@ async function getWindowsGPUData() {
return [];
}
}
export function isDiscreteBusAddress(busAddress?: string) {
if (!busAddress) {
return false;
}
if (!/^[0-9a-f]{2}:\d{2}\.\d$/i.test(busAddress)) {
return false;
}
const busNumber = parseInt(busAddress.substring(0, 2), 16);
const deviceNumber = parseInt(busAddress.substring(3, 5), 16);
if (busNumber === 0 && deviceNumber === 2) {
return false;
}
if (busNumber > 0 && busNumber <= 15) {
return true;
}
return false;
}

View file

@ -66,14 +66,14 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
items.push({
label: 'CUDA',
value: gpuCapabilities.cuda.version
? `v${gpuCapabilities.cuda.version}`
? gpuCapabilities.cuda.version
: 'Available',
});
if (gpuCapabilities.cuda.driverVersion) {
items.push({
label: 'NVIDIA Driver',
value: `v${gpuCapabilities.cuda.driverVersion}`,
value: gpuCapabilities.cuda.driverVersion,
});
}
}
@ -82,14 +82,14 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
items.push({
label: 'ROCm',
value: gpuCapabilities.rocm.version
? `v${gpuCapabilities.rocm.version}`
? gpuCapabilities.rocm.version
: 'Available',
});
if (gpuCapabilities.rocm.driverVersion) {
items.push({
label: 'AMD Driver',
value: `v${gpuCapabilities.rocm.driverVersion}`,
value: gpuCapabilities.rocm.driverVersion,
});
}
}
@ -98,7 +98,7 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
items.push({
label: 'Vulkan',
value: gpuCapabilities.vulkan.version
? `v${gpuCapabilities.vulkan.version}`
? gpuCapabilities.vulkan.version
: 'Available',
});
} else {
@ -112,7 +112,7 @@ export const createDriverItems = (hardwareInfo: HardwareInfo) => {
items.push({
label: 'CLBlast',
value: gpuCapabilities.clblast.version
? `v${gpuCapabilities.clblast.version}`
? gpuCapabilities.clblast.version
: 'Available',
});
} else {
@ -135,18 +135,17 @@ export const createHardwareItems = (hardwareInfo: HardwareInfo) => [
},
{
label: 'RAM',
value:
hardwareInfo.systemMemory && hardwareInfo.systemMemory.totalGB > 0
? `${hardwareInfo.systemMemory.totalGB.toFixed(1)} GB${
hardwareInfo.systemMemory.type
? ` ${hardwareInfo.systemMemory.type}`
: ''
}${
hardwareInfo.systemMemory.speed
? ` @ ${hardwareInfo.systemMemory.speed} MHz`
: ''
}`
: 'Detecting...',
value: hardwareInfo.systemMemory
? `${hardwareInfo.systemMemory.totalGB} GB${
hardwareInfo.systemMemory.type
? ` ${hardwareInfo.systemMemory.type}`
: ''
}${
hardwareInfo.systemMemory.speed
? ` @ ${hardwareInfo.systemMemory.speed} MHz`
: ''
}`
: 'Detecting...',
},
...(hardwareInfo.gpu.gpuInfo.length > 0
? hardwareInfo.gpu.gpuInfo.map((gpu, index) => ({
@ -157,9 +156,7 @@ export const createHardwareItems = (hardwareInfo: HardwareInfo) => [
...(hardwareInfo.gpuMemory && hardwareInfo.gpuMemory.length > 0
? hardwareInfo.gpuMemory.map((mem, index) => ({
label: `VRAM ${index > 0 ? index + 1 : ''}`.trim(),
value: mem.totalMemoryGB
? `${mem.totalMemoryGB.toFixed(1)} GB`
: 'Unknown',
value: mem.totalMemoryGB ? `${mem.totalMemoryGB} GB` : 'Unknown',
}))
: []),
];