//go:build windows package gpu import ( "log" "os/exec" "strings" ) type gpuName struct { Name string Vendor string } func detectGPUNames() []gpuName { // Try PowerShell first, then wmic gpus := psGPUList() if len(gpus) > 0 { log.Printf("[gpu] PowerShell found %d GPUs", len(gpus)) for _, g := range gpus { log.Printf("[gpu] %s -> vendor=%s", g.Name, g.Vendor) } return gpus } gpus = wmicGPUList() if len(gpus) > 0 { log.Printf("[gpu] WMIC found %d GPUs", len(gpus)) for _, g := range gpus { log.Printf("[gpu] %s -> vendor=%s", g.Name, g.Vendor) } return gpus } log.Printf("[gpu] No GPUs detected via PowerShell or WMIC") return nil } func psGPUList() []gpuName { out, err := exec.Command("powershell", "-NoProfile", "-Command", "Get-CimInstance Win32_VideoController | Select-Object Name,AdapterCompatibility,DriverVersion | ConvertTo-Csv -NoTypeInformation", ).CombinedOutput() log.Printf("[gpu] PS output:\n%s", string(out)) if err != nil { log.Printf("[gpu] PS error: %v", err) return nil } return parseCSV(string(out)) } func wmicGPUList() []gpuName { out, err := exec.Command("wmic", "path", "win32_VideoController", "get", "Name,AdapterCompatibility", "/format:csv").CombinedOutput() log.Printf("[gpu] WMIC output:\n%s", string(out)) if err != nil { log.Printf("[gpu] WMIC error: %v", err) return nil } return parseWMIC(string(out)) } func parseCSV(out string) []gpuName { var gpus []gpuName seen := map[string]bool{} lines := strings.Split(out, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "\"Name\"") || strings.HasPrefix(line, "Name") { continue } // Remove quotes: "Name","AdapterCompatibility" line = strings.ReplaceAll(line, "\"", "") parts := strings.SplitN(line, ",", 2) if len(parts) < 1 { continue } name := strings.TrimSpace(parts[0]) vendor := "" if len(parts) >= 2 { vendor = strings.TrimSpace(parts[1]) } if name == "" || seen[name] { continue } if isVirtualGPU(name) { log.Printf("[gpu] SKIP virtual: %s", name) continue } vendor = classifyVendor(vendor, name) if vendor == "unknown" { log.Printf("[gpu] SKIP unknown vendor (vendor=%q name=%q)", vendor, name) continue } seen[name] = true gpus = append(gpus, gpuName{Name: name, Vendor: vendor}) } return gpus } func parseWMIC(out string) []gpuName { var gpus []gpuName seen := map[string]bool{} lines := strings.Split(out, "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "Node,") { continue } parts := strings.SplitN(line, ",", 3) if len(parts) < 3 { continue } name := strings.TrimSpace(parts[2]) vendor := strings.TrimSpace(parts[1]) if name == "" || seen[name] { continue } if isVirtualGPU(name) { log.Printf("[gpu] SKIP virtual: %s", name) continue } vendor = classifyVendor(vendor, name) if vendor == "unknown" { log.Printf("[gpu] SKIP unknown vendor (vendor=%q name=%q)", vendor, name) continue } seen[name] = true gpus = append(gpus, gpuName{Name: name, Vendor: vendor}) } return gpus } func classifyVendor(vendor, name string) string { lower := strings.ToLower(vendor + " " + name) switch { case strings.Contains(lower, "nvidia"): return "nvidia" case strings.Contains(lower, "intel") || strings.Contains(lower, "uhd graphics") || strings.Contains(lower, "iris") || strings.Contains(lower, "hd graphics"): return "intel" case strings.Contains(lower, "amd") || strings.Contains(lower, "radeon") || strings.Contains(lower, "ati"): return "amd" default: return "unknown" } } func isVirtualGPU(name string) bool { lower := strings.ToLower(name) // Filter out virtual/remote/display-only adapters that don't have encoders virtual := []string{ "virtual", "remote", "rdp", "citrix", "vmware", "hyper-v", "mirror", "indirect", "parsec", "splashtop", "idm", "mirage", "vnc", "displayonly", "basicdisplay", "microsoft basic", "microsoft remote", } for _, v := range virtual { if strings.Contains(lower, v) { return true } } return false }