package gpu import ( "ffmpeg-gui/internal/ffmpeg" "log" ) type GPUInfo struct { Name string `json:"name"` Vendor string `json:"vendor"` Encoders []EncoderCap `json:"encoders"` } type EncoderCap struct { Name string `json:"name"` Codec string `json:"codec"` Label string `json:"label"` Available bool `json:"available"` } type knownEnc struct{ Name, Vendor, Codec, Label string } var allEncoders = []knownEnc{ {"h264_nvenc", "nvidia", "h264", "H.264 NVENC"}, {"hevc_nvenc", "nvidia", "hevc", "HEVC NVENC"}, {"av1_nvenc", "nvidia", "av1", "AV1 NVENC"}, {"h264_qsv", "intel", "h264", "H.264 QSV"}, {"hevc_qsv", "intel", "hevc", "HEVC QSV"}, {"av1_qsv", "intel", "av1", "AV1 QSV"}, {"h264_amf", "amd", "h264", "H.264 AMF"}, {"hevc_amf", "amd", "hevc", "HEVC AMF"}, {"av1_amf", "amd", "av1", "AV1 AMF"}, } var cpuEncoders = []knownEnc{ {"libx264", "cpu", "h264", "H.264 (libx264)"}, {"libx265", "cpu", "hevc", "H.265/HEVC (libx265)"}, {"libsvtav1", "cpu", "av1", "AV1 (libsvtav1)"}, } func DetectGPUs(exec *ffmpeg.Executor) []GPUInfo { // Step 1: Get GPU names (DXGI/Registry/PS — no ffmpeg needed) gpuNames := detectGPUNames() log.Printf("[gpu] names: %v", gpuNames) // Step 2: Detect encoder availability via ffmpeg (best effort) var encList string var ffmpegOK bool if exec != nil { out, err := exec.RunSync("-encoders") if err == nil { encList = out ffmpegOK = true } else { log.Printf("[gpu] ffmpeg -encoders failed: %v (showing GPUs without encoder info)", err) } } // Fall back to ffmpeg if no GPU names were found if len(gpuNames) == 0 && ffmpegOK { gpuNames = ffmpegFallback(exec) } if len(gpuNames) == 0 { gpuNames = []gpuName{{Name: "Unknown GPU", Vendor: "unknown"}} } // Build result var result []GPUInfo for _, gn := range gpuNames { result = append(result, GPUInfo{ Name: gn.Name, Vendor: gn.Vendor, Encoders: capsForVendor(encList, gn.Vendor, ffmpegOK), }) } // CPU cpuName := detectCPUName() log.Printf("[gpu] CPU: %s", cpuName) result = append(result, GPUInfo{ Name: cpuName, Vendor: "cpu", Encoders: capsForVendor(encList, "cpu", ffmpegOK), }) return result } func capsForVendor(encList, vendor string, ffmpegOK bool) []EncoderCap { var src []knownEnc switch vendor { case "cpu": src = cpuEncoders default: src = allEncoders } var caps []EncoderCap for _, ke := range src { if ke.Vendor == vendor || vendor == "cpu" { avail := ffmpegOK && containsWord(encList, ke.Name) if !ffmpegOK && vendor == "cpu" && ke.Name == "libx264" { avail = true // always assume x264 is available } caps = append(caps, EncoderCap{ Name: ke.Name, Codec: ke.Codec, Label: ke.Label, Available: avail, }) } } return caps } func ffmpegFallback(exec ffmpegExecutor) []gpuName { out, err := exec.RunSync("-encoders") if err != nil { return nil } var gpus []gpuName if wordIn(out, "h264_nvenc") || wordIn(out, "hevc_nvenc") { gpus = append(gpus, gpuName{Name: "NVIDIA GPU", Vendor: "nvidia"}) } if wordIn(out, "h264_qsv") || wordIn(out, "hevc_qsv") { gpus = append(gpus, gpuName{Name: "Intel GPU", Vendor: "intel"}) } if wordIn(out, "h264_amf") || wordIn(out, "hevc_amf") { gpus = append(gpus, gpuName{Name: "AMD GPU", Vendor: "amd"}) } return gpus } type ffmpegExecutor interface { RunSync(args ...string) (string, error) } func containsWord(text, word string) bool { if len(word) == 0 || len(text) < len(word) { return false } for i := 0; i <= len(text)-len(word); i++ { if text[i:i+len(word)] == word { before := i == 0 || text[i-1] == ' ' || text[i-1] == '\n' || text[i-1] == '\r' after := i+len(word) >= len(text) || text[i+len(word)] == ' ' || text[i+len(word)] == '\n' || text[i+len(word)] == '\r' if before && after { return true } } } return false } func wordIn(text, word string) bool { return containsWord(text, word) }