From 3ef91d1198efd8889d27bdc5daa3b9413069649d Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 26 Sep 2025 03:15:38 -0700 Subject: [PATCH] more hardware + driver display improvements --- src/hooks/useWarnings.ts | 6 +- src/main/modules/hardware.ts | 352 +++++++++++++++++++++++---------- src/main/modules/monitoring.ts | 8 +- src/types/hardware.d.ts | 4 +- src/utils/node/gpu.ts | 68 +++++-- src/utils/systemInfo.ts | 39 ++-- 6 files changed, 334 insertions(+), 143 deletions(-) diff --git a/src/hooks/useWarnings.ts b/src/hooks/useWarnings.ts index b3fcf85..f6945de 100644 --- a/src/hooks/useWarnings.ts +++ b/src/hooks/useWarnings.ts @@ -109,15 +109,15 @@ const checkVramWarnings = async (backend: string): Promise => { 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({ diff --git a/src/main/modules/hardware.ts b/src/main/modules/hardware.ts index 3d2eddd..07d9a48 100644 --- a/src/main/modules/hardware.ts +++ b/src/main/modules/hardware.ts @@ -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), }; } }; diff --git a/src/main/modules/monitoring.ts b/src/main/modules/monitoring.ts index 878f264..d550188 100644 --- a/src/main/modules/monitoring.ts +++ b/src/main/modules/monitoring.ts @@ -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 = { diff --git a/src/types/hardware.d.ts b/src/types/hardware.d.ts index da94dc9..dd246e1 100644 --- a/src/types/hardware.d.ts +++ b/src/types/hardware.d.ts @@ -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; } diff --git a/src/utils/node/gpu.ts b/src/utils/node/gpu.ts index e0b84d6..55f2232 100644 --- a/src/utils/node/gpu.ts +++ b/src/utils/node/gpu.ts @@ -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; +} diff --git a/src/utils/systemInfo.ts b/src/utils/systemInfo.ts index 0a25947..dc81ef7 100644 --- a/src/utils/systemInfo.ts +++ b/src/utils/systemInfo.ts @@ -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', })) : []), ];