package platform import ( "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "time" "gitgud.io/mike/mpv-manager/pkg/log" ) type GPUInfo struct { Models []string Brand string SupportsVA bool SupportsNV bool SupportsVT bool SupportedCodecs []string } type Codec struct { Name string Profile string } type CodecSupport struct { API string Codecs []Codec } type HardwareDecoder struct { Vendor string API string Supported []string } type GPUModelEntry struct { ModelNames []string SeriesNames []string Codename string AltCodenames []string Architecture string Codecs []string } var codecProfiles = map[string][]string{ "av2": {"AV2Profile0"}, "vvc": {"VVCMain", "VVCMain10", "VVCMain12"}, "av1": {"AV1Profile0", "AV1Profile1"}, "vp9": {"VP9Profile0", "VP9Profile2", "VP9Profile1", "VP9Profile3"}, "hevc": {"HEVCMain", "HEVCMain10", "HEVCMain12", "HEVCMain422", "HEVCMain444", "HEVCMain444_10", "HEVCMain444_12", "HEVCSccMain", "HEVCSccMain10"}, "vp8": {"VP8Version0_3"}, "prores": {"ProRes422", "ProRes422HQ", "ProRes422LT", "ProRes422Proxy", "ProRes4444"}, "vc1": {"VC1Simple", "VC1Main", "VC1Advanced"}, "avc": {"H264ConstrainedBaseline", "H264Baseline", "H264Main", "H264High", "H264High10", "H264High422", "H264High444", "H264MultiviewHigh", "H264StereoHigh", "H264ConstrainedHigh"}, "mpeg2": {"MPEG2Simple", "MPEG2Main", "MPEG2High"}, } var codecNameMapping = map[string]string{ "H.264": "avc", "HEVC": "hevc", "VP9": "vp9", "AV1": "av1", "VP8": "vp8", "VC-1": "vc1", "MPEG-2": "mpeg2", "VVC": "vvc", } func convertCodecsToMPVFormat(csvCodecs string) []string { parts := strings.Split(csvCodecs, ",") var codecs []string for _, part := range parts { codecName := strings.TrimSpace(part) if mapped, ok := codecNameMapping[codecName]; ok { codecs = append(codecs, mapped) } } return codecs } func parseGlxInfo(output string) []string { var models []string lines := strings.Split(output, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "Device:") || strings.HasPrefix(line, "OpenGL renderer string:") { var gpuName string if strings.HasPrefix(line, "Device:") { gpuName = strings.TrimSpace(strings.TrimPrefix(line, "Device:")) } else { gpuName = strings.TrimSpace(strings.TrimPrefix(line, "OpenGL renderer string:")) } if idx := strings.Index(gpuName, "("); idx > 0 { gpuName = strings.TrimSpace(gpuName[:idx]) } if gpuName != "" && !contains(models, gpuName) { models = append(models, gpuName) } } } return models } func parseVulkanInfo(output string) []string { var models []string lines := strings.Split(output, "\n") for _, line := range lines { line = strings.TrimSpace(line) if strings.Contains(line, "deviceName") && strings.Contains(line, "=") { parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { gpuName := strings.TrimSpace(parts[1]) if idx := strings.Index(gpuName, "("); idx > 0 { gpuName = strings.TrimSpace(gpuName[:idx]) } if gpuName != "" && !contains(models, gpuName) { models = append(models, gpuName) } } } } return models } func lookupGPUCodecs(gpuName string, gpuData []GPUModelEntry) ([]string, bool) { gpuNameLower := strings.ToLower(strings.TrimSpace(gpuName)) for _, entry := range gpuData { for _, modelName := range entry.ModelNames { if strings.Contains(gpuNameLower, strings.ToLower(modelName)) { return entry.Codecs, true } } } for _, entry := range gpuData { for _, seriesName := range entry.SeriesNames { if strings.Contains(gpuNameLower, strings.ToLower(seriesName)) { return entry.Codecs, true } } } for _, entry := range gpuData { if strings.Contains(gpuNameLower, strings.ToLower(entry.Codename)) { return entry.Codecs, true } } for _, entry := range gpuData { for _, altCodename := range entry.AltCodenames { if strings.Contains(gpuNameLower, strings.ToLower(altCodename)) { return entry.Codecs, true } } } return []string{"unknown"}, false } func lookupGPUCodecsWithFallback(gpuName string, brand string) ([]string, bool) { switch strings.ToLower(brand) { case "amd": if codecs, matched := lookupGPUCodecs(gpuName, amdGPUData); matched { return codecs, true } case "nvidia": if codecs, matched := lookupGPUCodecs(gpuName, nvidiaGPUData); matched { return codecs, true } case "intel": if codecs, matched := lookupGPUCodecs(gpuName, intelGPUData); matched { return codecs, true } } allData := [][]GPUModelEntry{amdGPUData, nvidiaGPUData, intelGPUData} for _, data := range allData { if codecs, matched := lookupGPUCodecs(gpuName, data); matched { return codecs, true } } // Log when no codec database match is found for this GPU log.Debug(fmt.Sprintf("[GPU] No codec database match found for GPU: %q (brand: %s), using fallback", gpuName, brand)) return []string{"unknown"}, false } var amdGPUData = []GPUModelEntry{ { ModelNames: []string{"Radeon RX 480", "Radeon RX 470", "Radeon RX 590", "Radeon RX 580", "Radeon RX 570"}, SeriesNames: []string{"RX 480", "RX 470", "RX 590", "RX 580", "RX 570"}, Codename: "Polaris 10", AltCodenames: []string{"Polaris 20", "Polaris 30"}, Architecture: "GCN 4.0", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 460", "Radeon RX 560", "Radeon RX 550", "Radeon RX 540"}, SeriesNames: []string{"RX 460", "RX 560", "RX 550", "RX 540"}, Codename: "Polaris 11", AltCodenames: []string{"Polaris 12", "Polaris 21"}, Architecture: "GCN 4.0", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX Vega 64", "Radeon RX Vega 56"}, SeriesNames: []string{"RX Vega 64", "RX Vega 56", "Vega 64", "Vega 56"}, Codename: "Vega 10", Architecture: "GCN 5.0", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { // Intel Kaby Lake-G processors with integrated AMD Radeon Vega M GPUs // These hybrid chips combine Intel CPU with AMD Vega graphics on same package // Examples: i5-8705G, i7-8705G, i7-8809G ModelNames: []string{"Radeon RX Vega M GL", "Radeon RX Vega M MX", "Vega M GL", "Vega M MX"}, SeriesNames: []string{"RX Vega M GL", "RX Vega M MX", "Vega M GL", "Vega M MX", "Vega M"}, Codename: "Vega M", AltCodenames: []string{"Kaby Lake-G"}, Architecture: "GCN 5.0", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon VII"}, SeriesNames: []string{"Radeon VII"}, Codename: "Vega 20", Architecture: "GCN 5.0", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 5700 XT", "Radeon RX 5700", "Radeon RX 5600 XT"}, SeriesNames: []string{"RX 5700 XT", "RX 5700", "RX 5600 XT", "RX 5700", "RX 5600"}, Codename: "Navi 10", Architecture: "RDNA 1", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 5500 XT", "Radeon RX 5500", "Radeon RX 5300"}, SeriesNames: []string{"RX 5500 XT", "RX 5500", "RX 5300", "RX 5500", "RX 5300"}, Codename: "Navi 14", Architecture: "RDNA 1", Codecs: []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 6950 XT", "Radeon RX 6900 XT", "Radeon RX 6800 XT"}, SeriesNames: []string{"RX 6950 XT", "RX 6900 XT", "RX 6800 XT", "RX 6900", "RX 6800"}, Codename: "Navi 21", Architecture: "RDNA 2", Codecs: []string{"avc", "hevc", "vp9", "av1", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 6750 XT", "Radeon RX 6700 XT", "Radeon RX 6700"}, SeriesNames: []string{"RX 6750 XT", "RX 6700 XT", "RX 6700", "RX 6750", "RX 6700"}, Codename: "Navi 22", Architecture: "RDNA 2", Codecs: []string{"avc", "hevc", "vp9", "av1", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 6650 XT", "Radeon RX 6600 XT", "Radeon RX 6600"}, SeriesNames: []string{"RX 6650 XT", "RX 6600 XT", "RX 6600", "RX 6650", "RX 6600"}, Codename: "Navi 23", Architecture: "RDNA 2", Codecs: []string{"avc", "hevc", "vp9", "av1", "vc1", "mpeg2"}, }, { ModelNames: []string{"Radeon RX 6500 XT", "Radeon RX 6400"}, SeriesNames: []string{"RX 6500 XT", "RX 6400", "RX 6500", "RX 6400"}, Codename: "Navi 24", Architecture: "RDNA 2", Codecs: []string{"avc", "hevc", "vp9"}, }, { ModelNames: []string{"Radeon RX 7900 XTX", "Radeon RX 7900 XT", "Radeon RX 7900 GRE", "Radeon RX 7900M"}, SeriesNames: []string{"RX 7900 XTX", "RX 7900 XT", "RX 7900 GRE", "RX 7900M", "RX 7900"}, Codename: "Navi 31", Architecture: "RDNA 3", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { ModelNames: []string{"Radeon RX 7800 XT", "Radeon RX 7700 XT"}, SeriesNames: []string{"RX 7800 XT", "RX 7700 XT", "RX 7800", "RX 7700"}, Codename: "Navi 32", Architecture: "RDNA 3", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { ModelNames: []string{"Radeon RX 7600 XT", "Radeon RX 7600"}, SeriesNames: []string{"RX 7600 XT", "RX 7600", "RX 7600"}, Codename: "Navi 33", Architecture: "RDNA 3", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { ModelNames: []string{"Radeon RX 9070 XT", "Radeon RX 9070"}, SeriesNames: []string{"RX 9070 XT", "RX 9070", "RX 9070"}, Codename: "Navi 48", Architecture: "RDNA 4", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { ModelNames: []string{"Radeon RX 9060 XT", "Radeon RX 9060"}, SeriesNames: []string{"RX 9060 XT", "RX 9060", "RX 9060"}, Codename: "Navi 44", Architecture: "RDNA 4", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, } var intelGPUData = []GPUModelEntry{ { ModelNames: []string{"HD Graphics 2000", "HD Graphics 3000"}, SeriesNames: []string{"HD Graphics 2000", "HD Graphics 3000"}, Codename: "Sandy Bridge", Architecture: "Gen 6", Codecs: []string{"avc", "vc1", "mpeg2"}, }, { ModelNames: []string{"HD Graphics 2500", "HD Graphics 4000"}, SeriesNames: []string{"HD Graphics 2500", "HD Graphics 4000"}, Codename: "Ivy Bridge", Architecture: "Gen 7", Codecs: []string{"avc", "vc1", "mpeg2"}, }, { ModelNames: []string{"HD Graphics 4200", "HD Graphics 4400", "HD Graphics 4600", "HD Graphics 5000", "HD Graphics 5100", "HD Graphics 5200"}, SeriesNames: []string{"HD Graphics 4200", "HD Graphics 4400", "HD Graphics 4600", "HD Graphics 5000", "HD Graphics 5100", "HD Graphics 5200"}, Codename: "Haswell", Architecture: "Gen 7.5", Codecs: []string{"avc", "vc1", "mpeg2"}, }, { ModelNames: []string{"HD Graphics 5300", "HD Graphics 5500", "HD Graphics 6000", "HD Graphics 6100", "HD Graphics 6200", "HD Graphics 6300"}, SeriesNames: []string{"HD Graphics 5300", "HD Graphics 5500", "HD Graphics 6000", "HD Graphics 6100", "HD Graphics 6200", "HD Graphics 6300"}, Codename: "Broadwell", Architecture: "Gen 8", Codecs: []string{"avc", "vp8", "vc1", "mpeg2"}, }, { ModelNames: []string{"HD Graphics 510", "HD Graphics 515", "HD Graphics 520", "HD Graphics 530", "HD Graphics 540", "HD Graphics 550", "HD Graphics P530", "HD Graphics P580"}, SeriesNames: []string{"HD Graphics 510", "HD Graphics 515", "HD Graphics 520", "HD Graphics 530", "HD Graphics 540", "HD Graphics 550"}, Codename: "Skylake", Architecture: "Gen 9", Codecs: []string{"avc", "hevc", "vp8", "vc1", "mpeg2"}, }, { ModelNames: []string{"UHD Graphics 610", "UHD Graphics 615", "UHD Graphics 620", "UHD Graphics 630", "UHD Graphics 640", "UHD Graphics 650", "UHD Graphics 655"}, SeriesNames: []string{"UHD Graphics 610", "UHD Graphics 615", "UHD Graphics 620", "UHD Graphics 630", "UHD Graphics 640", "UHD Graphics 650", "UHD Graphics 655", "UHD 610", "UHD 615", "UHD 620", "UHD 630"}, Codename: "Kaby Lake", AltCodenames: []string{"Coffee Lake", "Comet Lake"}, Architecture: "Gen 9.5", Codecs: []string{"avc", "hevc", "vp9", "vp8", "vc1", "mpeg2"}, }, { ModelNames: []string{"Iris Plus Graphics 645", "Iris Plus Graphics 655", "Iris Plus Graphics G4", "Iris Plus Graphics G7"}, SeriesNames: []string{"Iris Plus Graphics 645", "Iris Plus Graphics 655", "Iris Plus Graphics G4", "Iris Plus Graphics G7", "Iris Plus"}, Codename: "Ice Lake", Architecture: "Gen 11", Codecs: []string{"avc", "hevc", "vp9", "vp8", "vc1", "mpeg2"}, }, { ModelNames: []string{"UHD Graphics 700", "UHD 700", "Iris Xe Graphics", "Iris Xe"}, SeriesNames: []string{"UHD Graphics 700", "UHD 700", "UHD 770", "UHD 750", "UHD 730", "UHD 710", "Iris Xe Graphics", "Iris Xe", "Iris Xe"}, Codename: "Tiger Lake", AltCodenames: []string{"Alder Lake", "Raptor Lake"}, Architecture: "Gen 12.1 (Xe-LP)", Codecs: []string{"avc", "hevc", "vp9", "av1", "vc1", "mpeg2"}, }, { ModelNames: []string{"Arc A770", "Arc A750", "Arc A550", "Arc A380", "Arc A350", "Arc Graphics", "Arc"}, SeriesNames: []string{"Arc A770", "Arc A750", "Arc A550", "Arc A380", "Arc A350", "Arc Graphics", "Arc", "Arc A"}, Codename: "Alchemist", AltCodenames: []string{"Meteor Lake"}, Architecture: "Xe-HPG / Xe-LPG", Codecs: []string{"avc", "hevc", "vp9", "av1", "mpeg2"}, }, { ModelNames: []string{"Core Ultra 7 155H", "Core Ultra 7 165H", "Core Ultra 9 185H", "Core Ultra", "Lunar Lake"}, SeriesNames: []string{"Core Ultra 7", "Core Ultra 9", "Core Ultra", "Lunar Lake"}, Codename: "Lunar Lake", Architecture: "Xe2-LPG", Codecs: []string{"avc", "hevc", "vp9", "av1", "vvc", "mpeg2"}, }, { ModelNames: []string{"Arc B580", "Arc B570", "Arc B-Series", "Battlemage"}, SeriesNames: []string{"Arc B580", "Arc B570", "Arc B-Series", "Battlemage", "Arc B"}, Codename: "Battlemage", Architecture: "Xe2-HPG", Codecs: []string{"avc", "hevc", "vp9", "av1", "mpeg2"}, }, } var nvidiaGPUData = []GPUModelEntry{ { ModelNames: []string{"GeForce GTX 600", "GeForce GTX 700", "GeForce Titan", "GTX 600", "GTX 700"}, SeriesNames: []string{"GeForce GTX 600", "GeForce GTX 700", "GeForce Titan", "GTX 600", "GTX 700", "Titan"}, Codename: "GK104", AltCodenames: []string{"GK106", "GK107", "GK110", "GK208"}, Architecture: "Kepler", Codecs: []string{"avc", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce GTX 750", "GeForce GTX 750 Ti"}, SeriesNames: []string{"GeForce GTX 750", "GeForce GTX 750 Ti", "GTX 750", "GTX 750 Ti"}, Codename: "GM107", AltCodenames: []string{"GM108"}, Architecture: "Maxwell (1st Gen)", Codecs: []string{"avc", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce GTX 970", "GeForce GTX 980", "GeForce Titan X"}, SeriesNames: []string{"GeForce GTX 970", "GeForce GTX 980", "GeForce Titan X", "GTX 970", "GTX 980", "Titan X"}, Codename: "GM200", AltCodenames: []string{"GM204"}, Architecture: "Maxwell (2nd Gen)", Codecs: []string{"avc", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce GTX 950", "GeForce GTX 960"}, SeriesNames: []string{"GeForce GTX 950", "GeForce GTX 960", "GTX 950", "GTX 960"}, Codename: "GM206", Architecture: "Maxwell (2nd Gen)", Codecs: []string{"avc", "hevc", "vp9", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce GTX 1000", "GeForce GT 1030", "GTX 1000"}, SeriesNames: []string{"GeForce GTX 1000", "GeForce GT 1030", "GTX 1000", "GTX 1080", "GTX 1070", "GTX 1060", "GTX 1050"}, Codename: "GP102", AltCodenames: []string{"GP104", "GP106", "GP107", "GP108"}, Architecture: "Pascal", Codecs: []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"NVIDIA Titan V"}, SeriesNames: []string{"NVIDIA Titan V", "Titan V"}, Codename: "GV100", Architecture: "Volta", Codecs: []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce RTX 20", "GeForce GTX 16", "RTX 20", "GTX 16"}, SeriesNames: []string{"GeForce RTX 20", "GeForce GTX 16", "RTX 20", "GTX 16", "RTX 2080", "RTX 2070", "RTX 2060", "GTX 1660", "GTX 1650"}, Codename: "TU102", AltCodenames: []string{"TU104", "TU106", "TU116", "TU117"}, Architecture: "Turing", Codecs: []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce RTX 30", "RTX 30"}, SeriesNames: []string{"GeForce RTX 30", "RTX 30", "RTX 3090", "RTX 3080", "RTX 3070", "RTX 3060"}, Codename: "GA102", AltCodenames: []string{"GA104", "GA106", "GA107"}, Architecture: "Ampere", Codecs: []string{"avc", "hevc", "vp9", "av1", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce RTX 40", "RTX 40"}, SeriesNames: []string{"GeForce RTX 40", "RTX 40", "RTX 4090", "RTX 4080", "RTX 4070", "RTX 4060"}, Codename: "AD102", AltCodenames: []string{"AD103", "AD104", "AD106", "AD107"}, Architecture: "Ada Lovelace", Codecs: []string{"avc", "hevc", "vp9", "av1", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"GeForce RTX 50", "RTX 50"}, SeriesNames: []string{"GeForce RTX 50", "RTX 50", "RTX 5090", "RTX 5080", "RTX 5070"}, Codename: "GB202", AltCodenames: []string{"GB203", "GB205"}, Architecture: "Blackwell", Codecs: []string{"avc", "hevc", "vp9", "av1", "vp8", "mpeg2", "vc1"}, }, } func detectGPU() *GPUInfo { info := &GPUInfo{ Models: []string{}, } if runtime.GOOS == "windows" { info = detectGPUWindows() } else if runtime.GOOS == "linux" { info = detectGPULinux() } else if runtime.GOOS == "darwin" { info = detectGPUDarwin() } codecs, err := detectCodecSupport(info) if err == nil && len(codecs) > 0 { info.SupportedCodecs = codecs } return info } func detectGPULinux() *GPUInfo { info := &GPUInfo{ Models: []string{}, } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if output, err := exec.CommandContext(ctx, "glxinfo", "-B").Output(); err == nil { if models := parseGlxInfo(string(output)); len(models) > 0 { info.Models = append(info.Models, models...) } } else { log.Debug("glxinfo command failed: " + err.Error()) } if len(info.Models) == 0 { if output, err := exec.CommandContext(ctx, "vulkaninfo", "--summary").Output(); err == nil { if models := parseVulkanInfo(string(output)); len(models) > 0 { info.Models = append(info.Models, models...) } } else { log.Debug("vulkaninfo command failed: " + err.Error()) } } if len(info.Models) == 0 { if output, err := exec.Command("lspci").Output(); err == nil { for _, line := range strings.Split(string(output), "\n") { if strings.Contains(strings.ToLower(line), "vga") || strings.Contains(strings.ToLower(line), "display") { if idx := strings.Index(line, ": "); idx > 0 { model := strings.TrimSpace(line[idx+2:]) if model != "" && !contains(info.Models, model) { info.Models = append(info.Models, model) } } } } } else { log.Debug("lspci command failed: " + err.Error()) } } if len(info.Models) == 0 { drmDir := "/sys/class/drm" if entries, err := os.ReadDir(drmDir); err == nil { for _, entry := range entries { if strings.HasPrefix(entry.Name(), "card") { devicePath := filepath.Join(drmDir, entry.Name(), "device") ueventPath := filepath.Join(devicePath, "uevent") if fileExists(ueventPath) { if data, err := os.ReadFile(ueventPath); err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "PCI_ID=") { pciID := strings.TrimPrefix(line, "PCI_ID=") model := getGPUModelFromPCIID(pciID) if model != "" && !contains(info.Models, model) { info.Models = append(info.Models, model) } } } } } } } } } if len(info.Models) == 1 { var filteredModels []string for _, model := range info.Models { modelLower := strings.ToLower(model) if !strings.Contains(modelLower, "ryzen") && !strings.Contains(modelLower, "core") && !strings.Contains(modelLower, "processor") && !strings.Contains(modelLower, "rapahel") && !strings.Contains(modelLower, "mendocino") { filteredModels = append(filteredModels, model) } } if len(filteredModels) > 0 { info.Models = filteredModels } } info.Brand = determineGPUBrand(info.Models) info.SupportsVA = info.Brand == "amd" || info.Brand == "intel" info.SupportsNV = info.Brand == "nvidia" return info } func detectGPUDarwin() *GPUInfo { info := &GPUInfo{ Models: []string{}, Brand: "apple", SupportsVT: true, } if runtime.GOARCH == "arm64" { if output, err := exec.Command("sysctl", "-n", "machdep.cpu.brand_string").Output(); err == nil { cpuModel := strings.TrimSpace(string(output)) info.Models = append(info.Models, cpuModel) return info } } if output, err := exec.Command("system_profiler", "SPDisplaysDataType", "-json").Output(); err == nil { var data struct { SPDisplaysDataType []struct { Model string `json:"_name"` } `json:"SPDisplaysDataType"` } if err := json.Unmarshal(output, &data); err == nil { for _, display := range data.SPDisplaysDataType { if display.Model != "" { info.Models = append(info.Models, display.Model) info.Brand = determineGPUBrand(info.Models) } } } } return info } func detectGPUWindows() *GPUInfo { info := &GPUInfo{ Models: []string{}, } cmd := exec.Command("wmic", "path", "win32_VideoController", "get", "name") if output, err := cmd.Output(); err == nil { lines := strings.Split(string(output), "\n") for i, line := range lines { if i == 0 || strings.TrimSpace(line) == "" { continue } model := strings.TrimSpace(line) if model != "" && !contains(info.Models, model) { info.Models = append(info.Models, model) } } } if len(info.Models) == 0 { cmd = exec.Command("powershell", "-Command", "Get-WmiObject Win32_VideoController | Select-Object Name") if output, err := cmd.Output(); err == nil { lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.TrimSpace(line) != "" && !strings.Contains(line, "Name") && !strings.Contains(line, "---") { model := strings.TrimSpace(line) if model != "" && !contains(info.Models, model) { info.Models = append(info.Models, model) } } } } } info.Brand = determineGPUBrand(info.Models) info.SupportsVA = info.Brand == "amd" || info.Brand == "intel" info.SupportsNV = info.Brand == "nvidia" return info } func determineGPUBrand(models []string) string { if len(models) == 0 { return "" } brands := make(map[string]int) hasArc := false for _, model := range models { lowerModel := strings.ToLower(model) if strings.Contains(lowerModel, "nvidia") || strings.Contains(lowerModel, "geforce") || strings.Contains(lowerModel, "quadro") || strings.Contains(lowerModel, "tesla") { brands["nvidia"]++ } else if strings.Contains(lowerModel, "amd") || strings.Contains(lowerModel, "radeon") || strings.Contains(lowerModel, "advanced micro devices") { brands["amd"]++ } else if strings.Contains(lowerModel, "intel") { if strings.Contains(lowerModel, "arc") { hasArc = true } else { brands["intel"]++ } } else if strings.Contains(lowerModel, "apple") { brands["apple"]++ } } if brands["amd"] > 0 && brands["intel"] > 0 { if hasArc { return "intel" } return "amd" } if brands["amd"] > 0 && brands["nvidia"] > 0 { return "nvidia" } if brands["intel"] > 0 && brands["nvidia"] > 0 { return "nvidia" } for brand, count := range brands { if count > 0 { return brand } } return "" } func GetHWAOptions(ostype OSType, brand string) []string { var options []string if brand == "apple" && ostype == Darwin { options = []string{"videotoolbox", "videotoolbox-copy"} } else if ostype == Linux { if brand == "nvidia" { // Include ,auto variants for Nvidia on Linux options = []string{ "nvdec", "nvdec,auto", "nvdec-copy", "nvdec-copy,auto", "vulkan", "vulkan,auto", "vulkan-copy", "vulkan-copy,auto", } } else if brand == "amd" || brand == "intel" { options = []string{ "vaapi", "vaapi,auto", "vaapi-copy", "vaapi-copy,auto", "vulkan", "vulkan,auto", "vulkan-copy", "vulkan-copy,auto", "drm", "drm,auto", "drm-copy", "drm-copy,auto", } } else { options = []string{ "vaapi", "vaapi,auto", "nvdec", "nvdec,auto", "vulkan", "vulkan,auto", "vaapi-copy", "vaapi-copy,auto", "nvdec-copy", "nvdec-copy,auto", "vulkan-copy", "vulkan-copy,auto", } } } else if ostype == Windows { // Include ,auto variants for Windows options = []string{ "d3d11va", "d3d11va,auto", "d3d11va-copy", "d3d11va-copy,auto", "vulkan", "vulkan,auto", "vulkan-copy", "vulkan-copy,auto", } if brand == "nvidia" { options = append([]string{ "auto-safe", "nvdec", "nvdec,auto", "nvdec-copy", "nvdec-copy,auto", }, options...) } } return options } // GetRecommendedHWADecoder returns the recommended hardware decoder based on OS and GPU vendor. // The returned value may include an ",auto" suffix which is MPV's fallback mechanism - // if the primary decoder fails, it falls back to auto. // // Platform-specific recommendations: // - Windows + Nvidia: nvdec-copy,auto // - Windows + AMD: vulkan,auto // - Windows + Intel: vulkan,auto // - Linux + Nvidia: nvdec,auto // - Linux + AMD: auto // - Linux + Intel: auto // - macOS (All GPUs): auto // - Unknown brands: auto func GetRecommendedHWADecoder(ostype OSType, brand string) string { switch ostype { case Windows: switch strings.ToLower(brand) { case "nvidia": return "nvdec-copy,auto" case "amd", "intel": return "vulkan,auto" default: return "auto" } case Linux: switch strings.ToLower(brand) { case "nvidia": return "nvdec,auto" case "amd", "intel": return "auto" default: return "auto" } case Darwin: // macOS VideoToolbox works well with auto return "auto" default: return "auto" } } // GetHwaDescription returns a human-readable description for a hardware acceleration method. // This function consolidates descriptions used in both TUI and Web UI, providing consistent // documentation across all interfaces. The matching is case-insensitive, and unknown methods // are returned as-is for forward compatibility. // Methods with ",auto" suffix get the base description with " (fallback to auto)" appended. func GetHwaDescription(method string) string { methodLower := strings.ToLower(method) // Check for ,auto suffix and handle specially if strings.HasSuffix(methodLower, ",auto") { baseMethod := strings.TrimSuffix(methodLower, ",auto") baseDesc := getBaseHwaDescription(baseMethod) return baseDesc + " (fallback to auto)" } return getBaseHwaDescription(methodLower) } // getBaseHwaDescription returns the description for a base hwdec method (without ,auto suffix) func getBaseHwaDescription(method string) string { switch method { // Automatic selection methods case "auto": return "Let MPV choose best method" case "auto-safe": return "Safe automatic selection (NVIDIA only)" // NVIDIA CUDA decoding case "nvdec": return "NVIDIA Video Decode" case "nvdec-copy": return "NVIDIA Video Decode with system RAM copy" // Video Acceleration API (Linux AMD/Intel) case "vaapi": return "Video Acceleration API (Linux/AMD/Intel)" case "vaapi-copy": return "Video Acceleration API with system RAM copy" // Vulkan video decoding (cross-platform) case "vulkan": return "Vulkan video decoding" case "vulkan-copy": return "Vulkan with system RAM copy" // Direct3D 11 Video Acceleration (Windows) case "d3d11va": return "Direct3D 11 Video Acceleration (Windows)" case "d3d11va-copy": return "Direct3D 11 Video Acceleration with system RAM copy" // Apple VideoToolbox (macOS) case "videotoolbox": return "Apple VideoToolbox (macOS)" case "videotoolbox-copy": return "Apple VideoToolbox with system RAM copy" // Direct Rendering Manager (Linux AMD/Intel) case "drm": return "Direct Rendering Manager (Linux/AMD/Intel)" case "drm-copy": return "Direct Rendering Manager with system RAM copy" // Disable hardware decoding case "no": return "Disable hardware acceleration (CPU only)" // Unknown method - return as-is for forward compatibility default: return method } } func detectCodecSupport(info *GPUInfo) ([]string, error) { switch runtime.GOOS { case "linux": return parseCodecSupportLinux(info) case "windows": return parseCodecSupportWindows(info) case "darwin": return detectCodecSupportDarwin() default: return nil, fmt.Errorf("unsupported OS: %s", runtime.GOOS) } } func parseCodecSupportLinux(info *GPUInfo) ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if output, err := exec.CommandContext(ctx, "vainfo").Output(); err == nil { if detected, err := parseVainfo(string(output)); err == nil && len(detected) > 0 { return detected, nil } } if output, err := exec.CommandContext(ctx, "vdpauinfo").Output(); err == nil { if detected, err := parseVDPAUInfo(string(output)); err == nil && len(detected) > 0 { return detected, nil } } if len(info.Models) > 0 { brand := info.Brand for _, model := range info.Models { if codecs, matched := lookupGPUCodecsWithFallback(model, brand); matched { return codecs, nil } } } // Log when all detection methods fail and we fall back to unknown log.Debug("[GPU] HWA codec detection failed on Linux (no vainfo, vdpauinfo, or GPU match), falling back to 'unknown'") return []string{"unknown"}, nil } func parseVainfo(output string) ([]string, error) { var detected []string lines := strings.Split(output, "\n") for _, line := range lines { if !strings.Contains(line, "VAEntrypointVLD") { continue } for codec, profiles := range codecProfiles { for _, profile := range profiles { if strings.Contains(line, profile) { if !contains(detected, codec) { detected = append(detected, codec) } break } } } } if len(detected) == 0 { return nil, fmt.Errorf("no codecs found in VA-API output") } return detected, nil } func parseVDPAUInfo(output string) ([]string, error) { var detected []string lines := strings.Split(output, "\n") vdpauProfileMap := map[string]string{ "MPEG1": "mpeg2", "MPEG2": "mpeg2", "H264": "avc", "VC1": "vc1", "VP8": "vp8", "VP9": "vp9", "HEVC": "hevc", "AV1": "av1", } for _, line := range lines { for vdpauName, codec := range vdpauProfileMap { if strings.Contains(line, vdpauName) { if !contains(detected, codec) { detected = append(detected, codec) } } } } if len(detected) == 0 { return nil, fmt.Errorf("no codecs found in VDPAU output") } return detected, nil } func parseCodecSupportWindows(info *GPUInfo) ([]string, error) { _, err := exec.Command("powershell", "-Command", "Get-CimInstance Win32_VideoController | Select-Object Name").Output() if err != nil { return nil, err } brand := info.Brand for _, model := range info.Models { if codecs, matched := lookupGPUCodecsWithFallback(model, brand); matched { return codecs, nil } } // Log when no GPU model matched and we fall back to unknown log.Debug("[GPU] HWA codec detection failed on Windows (no GPU database match), falling back to 'unknown'") return []string{"unknown"}, nil } func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } func contains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false } func getGPUModelFromPCIID(pciID string) string { return "" } func max(a, b int) int { if a > b { return a } return b }