This commit is contained in:
sansen
2026-07-29 10:46:19 +08:00
parent 2c4dd55f11
commit 83f5bb8b30
14 changed files with 593 additions and 167 deletions
+123
View File
@@ -0,0 +1,123 @@
package gpu
import (
"ffmpeg-gui/internal/ffmpeg"
"log"
)
// GPUInfo holds detected GPU information.
type GPUInfo struct {
Name string `json:"name"`
Vendor string `json:"vendor"`
Encoders []EncoderCap `json:"encoders"`
}
// EncoderCap describes an encoder capability.
type EncoderCap struct {
Name string `json:"name"`
Codec string `json:"codec"`
Label string `json:"label"`
Available bool `json:"available"`
}
// knownEncoders defines all known hardware encoders by vendor.
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"},
}
// DetectGPUs detects GPUs and their encoder capabilities.
// Uses a single ffmpeg -encoders call for speed.
func DetectGPUs(exec *ffmpeg.Executor) []GPUInfo {
encList, err := exec.RunSync("-encoders")
if err != nil {
log.Printf("[gpu] ffmpeg -encoders failed: %v", err)
return fallbackGPUs()
}
// Group encoders by vendor
vendorEncs := map[string][]EncoderCap{}
for _, ke := range allEncoders {
available := containsWord(encList, ke.Name)
vendorEncs[ke.Vendor] = append(vendorEncs[ke.Vendor], EncoderCap{
Name: ke.Name, Codec: ke.Codec, Label: ke.Label, Available: available,
})
}
// Get GPU names
gpuNames := detectGPUNames()
log.Printf("[gpu] detected GPU names: %v", gpuNames)
if len(gpuNames) == 0 {
gpuNames = []gpuName{{Name: "Unknown GPU", Vendor: "unknown"}}
}
var result []GPUInfo
for _, gn := range gpuNames {
encs := vendorEncs[gn.Vendor]
if encs == nil {
encs = []EncoderCap{}
}
result = append(result, GPUInfo{
Name: gn.Name,
Vendor: gn.Vendor,
Encoders: encs,
})
}
// Add CPU entry
result = append(result, GPUInfo{
Name: "CPU",
Vendor: "cpu",
Encoders: []EncoderCap{
{Name: "libx264", Codec: "h264", Label: "H.264 (libx264)", Available: containsWord(encList, "libx264")},
{Name: "libx265", Codec: "hevc", Label: "H.265/HEVC (libx265)", Available: containsWord(encList, "libx265")},
{Name: "libsvtav1", Codec: "av1", Label: "AV1 (libsvtav1)", Available: containsWord(encList, "libsvtav1")},
},
})
log.Printf("[gpu] returning %d GPU entries", len(result))
return result
}
func containsWord(text, word string) bool {
// Simple substring match — the encoder list has one encoder per line
return len(text) > 0 && len(word) > 0 && containsLine(text, word)
}
func containsLine(text, word string) bool {
for i := 0; i < len(text); i++ {
if i+len(word) > len(text) {
break
}
if text[i:i+len(word)] == word {
// Check word boundaries: should be preceded by space/newline and followed by space/newline
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 fallbackGPUs() []GPUInfo {
return []GPUInfo{
{Name: "CPU", Vendor: "cpu", Encoders: []EncoderCap{
{Name: "libx264", Codec: "h264", Label: "H.264 (libx264)", Available: true},
{Name: "libx265", Codec: "hevc", Label: "H.265/HEVC (libx265)", Available: true},
}},
}
}
+12
View File
@@ -0,0 +1,12 @@
//go:build !windows
package gpu
type gpuName struct {
Name string
Vendor string
}
func detectGPUNames() []gpuName {
return []gpuName{{Name: "Default GPU", Vendor: "unknown"}}
}
+164
View File
@@ -0,0 +1,164 @@
//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
}