From b04c09dd316a1cfbb90afd4b74251058e76286c8 Mon Sep 17 00:00:00 2001 From: Mark-Shun Date: Mon, 22 Jun 2026 19:33:34 +0200 Subject: [PATCH 1/2] Initial implementation of GPU device selection for interference --- electron/dependencyHandlers.ts | 6 +- electron/preload.ts | 1 + electron/utils.ts | 114 ++++++++++++++++++++++++++++++- package.json | 1 + src/App.tsx | 14 +++- src/components/AppModals.tsx | 10 ++- src/components/SettingsModal.tsx | 70 ++++++++++++++++++- src/electron.d.ts | 20 ++++-- src/hooks/useOutputResolution.ts | 7 +- src/hooks/useSettings.ts | 60 ++++++++++++++-- src/hooks/useVideoProcessing.ts | 5 +- 11 files changed, 287 insertions(+), 21 deletions(-) diff --git a/electron/dependencyHandlers.ts b/electron/dependencyHandlers.ts index 0f8211c..7705ba1 100644 --- a/electron/dependencyHandlers.ts +++ b/electron/dependencyHandlers.ts @@ -1,7 +1,7 @@ import { ipcMain } from 'electron'; import { logger } from './logger'; import { configManager } from './configManager'; -import { detectCudaSupport, pollGpuStats } from './utils'; +import { detectCudaSupport, enumerateGpus, pollGpuStats } from './utils'; import { createIpcHandler } from './ipcUtilities'; import { DependencyManager } from './dependencyManager'; import { PluginInstaller } from './pluginInstaller'; @@ -37,6 +37,10 @@ export function registerDependencyHandlers( return await pollGpuStats(); }); + ipcMain.handle('enumerate-gpus', async () => { + return await enumerateGpus(); + }); + ipcMain.handle('setup-dependencies', createIpcHandler( 'setup-dependencies', diff --git a/electron/preload.ts b/electron/preload.ts index e30713a..4740349 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -6,6 +6,7 @@ contextBridge.exposeInMainWorld('electronAPI', { checkDependencies: () => ipcRenderer.invoke('check-dependencies'), detectCudaSupport: () => ipcRenderer.invoke('detect-cuda-support'), getGpuStats: () => ipcRenderer.invoke('get-gpu-stats'), + enumerateGpus: () => ipcRenderer.invoke('enumerate-gpus'), setupDependencies: () => ipcRenderer.invoke('setup-dependencies'), onSetupProgress: (callback: (progress: any) => void) => { const listener = (event: any, progress: any) => callback(progress); diff --git a/electron/utils.ts b/electron/utils.ts index 8b9034d..af4c413 100644 --- a/electron/utils.ts +++ b/electron/utils.ts @@ -1,6 +1,7 @@ // electron/utils.ts -import { spawn, ChildProcess } from 'child_process'; +import { spawn } from 'child_process'; import * as path from 'path'; +import * as si from 'systeminformation'; import { logger } from './logger'; import { PATHS } from './constants'; @@ -147,6 +148,67 @@ export async function withLogSeparator( } } +/** + * A detected GPU device from WMI enumeration + */ +export interface GpuDevice { + index: number; + name: string; + adapterRAM: number; // MB, 0 if unknown + vendor: 'nvidia' | 'amd' | 'intel' | 'other'; +} + +/** + * Enumerates available GPUs via systeminformation. + * The controller array order matches DXGI adapter indices. + * Results are cached after the first successful call. + */ +let gpuCache: GpuDevice[] | null = null; + +export async function enumerateGpus(): Promise { + if (gpuCache) return gpuCache; + + try { + const graphics = await si.graphics(); + const devices: GpuDevice[] = []; + + graphics.controllers.forEach((controller, index) => { + const name = controller.model || controller.name || 'Unknown GPU'; + + // Filter out software renderers and basic display adapters + const lower = name.toLowerCase(); + if (lower.includes('microsoft') || lower.includes('basic')) return; + + const lowerVendor = (controller.vendor || '').toLowerCase(); + let vendor: GpuDevice['vendor'] = 'other'; + if (lowerVendor.includes('nvidia') || lower.includes('nvidia')) vendor = 'nvidia'; + else if (lowerVendor.includes('amd') || lower.includes('radeon')) vendor = 'amd'; + else if (lowerVendor.includes('intel') || lower.includes('intel')) vendor = 'intel'; + + devices.push({ + index, + name, + adapterRAM: controller.vram || 0, // MB, from systeminformation + vendor + }); + }); + + logger.info(`Enumerated ${devices.length} GPU(s): ${devices.map(g => g.name).join(', ')}`); + gpuCache = devices; + return devices; + } catch (error) { + logger.error(`GPU enumeration failed: ${error instanceof Error ? error.message : String(error)}`); + return []; + } +} + +/** + * Clears the cached GPU list. Useful for testing or manual refresh. + */ +export function clearGpuCache(): void { + gpuCache = null; +} + /** * GPU stats returned by pollGpuStats */ @@ -156,13 +218,53 @@ export interface GpuStats { gpuUtilization: number; } +/** + * Resolves the path to nvidia-smi.exe by checking PATH first, + * then falling back to the standard system location. + * Returns the command string (may be just 'nvidia-smi' if found on PATH) + * or null if not found anywhere. + */ +const NVIDIA_SMI_PATH = 'C:\\Windows\\System32\\nvidia-smi.exe'; + +async function resolveNvidiaSmiPath(): Promise { + // Try PATH first (fast path) + try { + const pathCheck = await new Promise((resolve) => { + const proc = spawn('nvidia-smi', ['--version'], { + shell: true, + windowsHide: true, + stdio: 'ignore' + }); + proc.on('close', (code) => resolve(code === 0)); + proc.on('error', () => resolve(false)); + setTimeout(() => { proc.kill(); resolve(false); }, 2000); + }); + if (pathCheck) return 'nvidia-smi'; + } catch { + // fall through + } + + // Fallback: standard system location + try { + const exists = require('fs').existsSync(NVIDIA_SMI_PATH); + if (exists) return NVIDIA_SMI_PATH; + } catch { + // fall through + } + + return null; +} + /** * Polls nvidia-smi for GPU memory and utilization stats. * Returns null if nvidia-smi is unavailable (non-NVIDIA systems). */ export async function pollGpuStats(): Promise { try { - const proc = spawn('nvidia-smi', [ + const nvidiaSmi = await resolveNvidiaSmiPath(); + if (!nvidiaSmi) return null; + + const proc = spawn(nvidiaSmi, [ '--query-gpu=memory.used,memory.total,utilization.gpu', '--format=csv,noheader,nounits' ], { @@ -211,8 +313,14 @@ export async function pollGpuStats(): Promise { */ export async function detectCudaSupport(): Promise { try { + const nvidiaSmi = await resolveNvidiaSmiPath(); + if (!nvidiaSmi) { + logger.info('nvidia-smi not found - no CUDA support'); + return false; + } + // Try to run nvidia-smi to detect NVIDIA GPU - const proc = spawn('nvidia-smi', ['--query-gpu=name', '--format=csv,noheader'], { + const proc = spawn(nvidiaSmi, ['--query-gpu=name', '--format=csv,noheader'], { shell: true, windowsHide: true }); diff --git a/package.json b/package.json index 38adbc7..296d884 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-resizable-panels": "^3.0.6", + "systeminformation": "^5.31.8", "zod": "^4.3.6" }, "build": { diff --git a/src/App.tsx b/src/App.tsx index 97369ad..0fd08a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -65,7 +65,7 @@ function App() { // Setup and initialization hooks const { consoleOutput, consoleEndRef, addConsoleLog } = useConsoleLog(); const { isSetupComplete, isCheckingDeps, hasCudaSupport, setupProgress, isSettingUp, handleSetup, pluginInstallError, handleRetryPlugins, handleContinueWithoutPlugins } = useSetup(addConsoleLog); - const { useDirectML, toggleDirectML, numStreams, updateNumStreams } = useSettings(hasCudaSupport); + const { useDirectML, toggleDirectML, numStreams, updateNumStreams, deviceId, updateDeviceId, availableGpus, isEnumerating } = useSettings(hasCudaSupport); const { privacyMode, togglePrivacyMode } = usePrivacyMode(); const { ffmpegArgs, @@ -185,6 +185,7 @@ function App() { selectedModel, colorimetry: colorimetrySettings, segment, + deviceId, }); // Destructure queue store for convenience @@ -482,6 +483,7 @@ function App() { useDirectML, filters, numStreams, + deviceId, onLog: addConsoleLog, onUpdateVideoInfo: setVideoInfo, onError: (message) => notify.error('Workflow Validation Error', message), @@ -545,7 +547,8 @@ function App() { filters, numStreams, startFrame, - endFrame + endFrame, + deviceId ); if (result.success && result.previewPath) { @@ -577,7 +580,8 @@ function App() { true, filters, numStreams, - segment + segment, + deviceId ); if (result.success) { @@ -933,6 +937,10 @@ function App() { numStreams={numStreams} onUpdateNumStreams={updateNumStreams} onToggleDirectML={handleToggleDirectML} + deviceId={deviceId} + onUpdateDeviceId={updateDeviceId} + availableGpus={availableGpus} + isEnumerating={isEnumerating} videoCompareArgs={videoCompareArgs} onUpdateVideoCompareArgs={handleUpdateVideoCompareArgs} onResetVideoCompareArgs={handleResetVideoCompareArgs} diff --git a/src/components/AppModals.tsx b/src/components/AppModals.tsx index 2e0b91d..bcde902 100644 --- a/src/components/AppModals.tsx +++ b/src/components/AppModals.tsx @@ -9,7 +9,7 @@ import { PluginsModal } from './PluginsModal'; import { UpdateNotificationModal } from './UpdateNotificationModal'; import { VsMlrtUpdateModal } from './VsMlrtUpdateModal'; import { FilterImportModal } from './FilterImportModal'; -import type { UpdateInfo, VsMlrtVersionInfo } from '../electron'; +import type { UpdateInfo, VsMlrtVersionInfo, GpuDevice } from '../electron'; interface AppModalsProps { // Import Model @@ -43,6 +43,10 @@ interface AppModalsProps { numStreams: number; onUpdateNumStreams: (streams: number) => void; onToggleDirectML: (value: boolean) => void; + deviceId: number; + onUpdateDeviceId: (value: number) => void; + availableGpus: GpuDevice[]; + isEnumerating: boolean; videoCompareArgs: string; onUpdateVideoCompareArgs: (args: string) => void; onResetVideoCompareArgs: () => void; @@ -124,6 +128,10 @@ export const AppModals = memo(function AppModals(props: AppModalsProps) { onResetDefaultOutputFolder={props.onResetDefaultOutputFolder} descriptiveNamingEnabled={props.descriptiveNamingEnabled} onUpdateDescriptiveNamingEnabled={props.onUpdateDescriptiveNamingEnabled} + deviceId={props.deviceId} + onUpdateDeviceId={props.onUpdateDeviceId} + availableGpus={props.availableGpus} + isEnumerating={props.isEnumerating} /> void; descriptiveNamingEnabled: boolean; onUpdateDescriptiveNamingEnabled: (enabled: boolean) => void; + deviceId: number; + onUpdateDeviceId: (value: number) => void; + availableGpus: GpuDevice[]; + isEnumerating: boolean; } type Tab = 'general' | 'processing'; @@ -35,6 +40,10 @@ export const SettingsModal = memo(({ onResetDefaultOutputFolder, descriptiveNamingEnabled, onUpdateDescriptiveNamingEnabled, + deviceId, + onUpdateDeviceId, + availableGpus, + isEnumerating, }) => { const [activeTab, setActiveTab] = useState('general'); const [showVideoCompareOptions, setShowVideoCompareOptions] = useState(false); @@ -199,6 +208,65 @@ export const SettingsModal = memo(({ )} + {/* GPU Device Selection */} +
+