package platform import ( "encoding/json" "fmt" "os" "path/filepath" "runtime" "strings" "unicode" "unicode/utf8" "gitgud.io/mike/mpv-manager/pkg/log" ) type GPUInfo struct { Models []string // Brands holds the detected GPU vendor per model (parallel to Models); // entries may be empty when the vendor cannot be determined. Brand below // remains the collapsed single brand for backward compatibility. Brands []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"}, } 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:")) } gpuName = stripTrailingGPUProbeAnnotation(gpuName) 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]) gpuName = stripTrailingGPUProbeAnnotation(gpuName) if gpuName != "" && !contains(models, gpuName) { models = append(models, gpuName) } } } } return models } func parseLSPCIModels(output string) []string { var models []string for _, line := range strings.Split(output, "\n") { lower := strings.ToLower(line) if !strings.Contains(lower, "vga compatible controller") && !strings.Contains(lower, "3d controller") && !strings.Contains(lower, "display controller") { continue } if index := strings.Index(line, ": "); index >= 0 { model := strings.TrimSpace(line[index+2:]) if revision := strings.LastIndex(strings.ToLower(model), " (rev "); revision > 0 && strings.HasSuffix(model, ")") { model = strings.TrimSpace(model[:revision]) } appendGPUModels(&models, model) } } return models } func appendGPUModels(destination *[]string, models ...string) { for _, model := range models { model = strings.TrimSpace(model) if model == "" { continue } duplicate := false for _, existing := range *destination { if sameGPUModel(existing, model) { duplicate = true break } } if !duplicate { *destination = append(*destination, model) } } } func sameGPUModel(left, right string) bool { normalize := func(value string) string { value = strings.ToLower(value) for _, decoration := range []string{"(r)", "(tm)", "(c)", "®", "™", "©"} { value = strings.ReplaceAll(value, decoration, "") } return strings.Join(strings.Fields(value), " ") } leftNormalized := normalize(left) rightNormalized := normalize(right) if leftNormalized == rightNormalized { return true } if determineModelBrand(left) == "" || determineModelBrand(left) != determineModelBrand(right) { return false } // Different probe tools decorate one adapter differently. A shared model // token containing a digit (3080, 780m, a770, ...) is sufficiently specific // to collapse those representations without merging two cards of one brand. digitTokens := make(map[string]bool) for _, token := range strings.FieldsFunc(leftNormalized, func(r rune) bool { return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') }) { if strings.IndexFunc(token, func(r rune) bool { return r >= '0' && r <= '9' }) >= 0 { digitTokens[token] = true } } for _, token := range strings.FieldsFunc(rightNormalized, func(r rune) bool { return !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') }) { if digitTokens[token] { return true } } return false } // stripTrailingGPUProbeAnnotation removes driver/PCI annotations appended as // a separate final parenthetical group while preserving trademark decorations // inside the actual model, e.g. "Radeon(TM) 780M (0x15bf)". func stripTrailingGPUProbeAnnotation(name string) string { name = strings.TrimSpace(name) if strings.HasSuffix(name, ")") { if index := strings.LastIndex(name, " ("); index > 0 { return strings.TrimSpace(name[:index]) } } return name } func lookupGPUCodecs(gpuName string, gpuData []GPUModelEntry) ([]string, bool) { gpuNameLower := strings.ToLower(strings.TrimSpace(gpuName)) // Strip trademark decorations and collapse whitespace so that names like // "Intel(R) Arc(TM) B580 Graphics" match table entries like "Arc B580". for _, deco := range []string{"(r)", "(tm)", "(c)", "®", "™", "©"} { gpuNameLower = strings.ReplaceAll(gpuNameLower, deco, "") } gpuNameLower = strings.Join(strings.Fields(gpuNameLower), " ") for _, entry := range gpuData { for _, modelName := range entry.ModelNames { if gpuAliasMatches(gpuNameLower, modelName) { return entry.Codecs, true } } } for _, entry := range gpuData { for _, seriesName := range entry.SeriesNames { if gpuAliasMatches(gpuNameLower, seriesName) { return entry.Codecs, true } } } for _, entry := range gpuData { if gpuAliasMatches(gpuNameLower, entry.Codename) { return entry.Codecs, true } } for _, entry := range gpuData { for _, altCodename := range entry.AltCodenames { if gpuAliasMatches(gpuNameLower, altCodename) { return entry.Codecs, true } } } return []string{"unknown"}, false } // gpuAliasMatches keeps model tokens intact: RX 6800 must not claim RX 6800M, // and Vega 6 must not claim Vega 64. Model-family prefixes do not imply a // capability: RTX 2050 and RTX 2060, for example, have different decoders. func gpuAliasMatches(name, alias string) bool { alias = strings.ToLower(alias) if alias == "" { return false } for offset := 0; offset < len(name); { index := strings.Index(name[offset:], alias) if index < 0 { return false } start := offset + index end := start + len(alias) left, _ := utf8.DecodeLastRuneInString(name[:start]) right, _ := utf8.DecodeRuneInString(name[end:]) leftBoundary := start == 0 || (!unicode.IsLetter(left) && !unicode.IsDigit(left)) rightBoundary := end == len(name) || (!unicode.IsLetter(right) && !unicode.IsDigit(right)) if leftBoundary && rightBoundary { return true } offset = start + 1 } return 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 } // Verified decoder mappings and limitations: docs/FEATURES/GPU_CODEC_DETECTION.md. // Model/series aliases match whole tokens; mobile suffixes need explicit aliases. 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 5700M", "Radeon RX 5600M", "Radeon RX 5700 XT", "Radeon RX 5700", "Radeon RX 5600 XT"}, SeriesNames: []string{"RX 5700M", "RX 5600M", "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 5500M", "Radeon RX 5300M", "Radeon RX 5500 XT", "Radeon RX 5500", "Radeon RX 5300"}, SeriesNames: []string{"RX 5500M", "RX 5300M", "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 6850M XT", "Radeon RX 6800M", "Radeon RX 6700M", "Radeon RX 6750 XT", "Radeon RX 6700 XT", "Radeon RX 6700"}, SeriesNames: []string{"RX 6850M XT", "RX 6800M", "RX 6700M", "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 6650M XT", "Radeon RX 6650M", "Radeon RX 6600M", "Radeon RX 6800S", "Radeon RX 6700S", "Radeon RX 6600S", "Radeon RX 6650 XT", "Radeon RX 6600 XT", "Radeon RX 6600"}, SeriesNames: []string{"RX 6650M XT", "RX 6650M", "RX 6600M", "RX 6800S", "RX 6700S", "RX 6600S", "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 6550M", "Radeon RX 6500M", "Radeon RX 6450M", "Radeon RX 6300M", "Radeon RX 6500 XT", "Radeon RX 6400"}, SeriesNames: []string{"RX 6550M", "RX 6500M", "RX 6450M", "RX 6300M", "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 7900M", "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 7800M", "Radeon RX 7800 XT", "Radeon RX 7700 XT"}, SeriesNames: []string{"RX 7800M", "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 7600M XT", "Radeon RX 7600M", "Radeon RX 7700S", "Radeon RX 7600S", "Radeon RX 7600 XT", "Radeon RX 7600"}, SeriesNames: []string{"RX 7600M XT", "RX 7600M", "RX 7700S", "RX 7600S", "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"}, }, { // RDNA 2 APU iGPU (Ryzen 6000 mobile); reported as e.g. // "AMD Radeon 680M" or "Radeon(TM) 680M" ModelNames: []string{"Radeon 660M", "660M", "Radeon 680M", "680M"}, SeriesNames: []string{"Radeon 680M", "680M"}, Codename: "Rembrandt", Architecture: "RDNA 2", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { // RDNA 3 APU iGPU (Ryzen 7040/8040 mobile), e.g. "AMD Radeon(TM) 780M" ModelNames: []string{"Radeon 760M", "760M", "Radeon 740M", "740M", "Radeon 780M", "780M"}, SeriesNames: []string{"Radeon 780M", "780M"}, Codename: "Phoenix", Architecture: "RDNA 3", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { // RDNA 3.5 APU iGPU (Ryzen AI 300 mobile), e.g. "AMD Radeon(TM) 890M" ModelNames: []string{"Radeon 880M", "880M", "Radeon 860M", "860M", "Radeon 890M", "890M"}, SeriesNames: []string{"Radeon 890M", "890M"}, Codename: "Strix Point", AltCodenames: []string{"Krackan Point"}, Architecture: "RDNA 3.5", Codecs: []string{"avc", "hevc", "vp9", "av1"}, }, { // APU iGPUs (Ryzen 2000-7000 G-series and mobile), e.g. // "AMD Radeon(TM) Vega 8 Graphics". Discrete RX Vega cards are claimed // by the earlier entries in the ModelNames pass. ModelNames: []string{"Radeon Vega 6", "Radeon Vega 7", "Radeon Vega 8", "Radeon Vega 10", "Radeon Vega 11", "Vega 6", "Vega 7", "Vega 8", "Vega 10", "Vega 11"}, SeriesNames: []string{"Vega 6", "Vega 7", "Vega 8", "Vega 10", "Vega 11"}, Codename: "Raven Ridge", AltCodenames: []string{"Picasso", "Renoir", "Cezanne", "Lucienne", "Barcelo"}, Architecture: "GCN 5.0", Codecs: []string{"avc", "hevc", "vp9"}, }, } 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 770", "UHD Graphics 750", "UHD Graphics 730", "UHD Graphics 710", "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"}, }, { // Alchemist (including mobile and Pro) has no VVC or VP8 decoder. ModelNames: []string{"Arc A770", "Arc A750", "Arc A580", "Arc A380", "Arc A310", "Arc A350M", "Arc A370M", "Arc A530M", "Arc A550M", "Arc A570M", "Arc A730M", "Arc A770M", "Arc Pro A30M", "Arc Pro A40", "Arc Pro A50", "Arc Pro A60", "Arc Pro A60M"}, Codename: "Alchemist", AltCodenames: []string{"DG2", "ACM-G10", "ACM-G11", "ACM-G12"}, Architecture: "Xe-HPG", Codecs: []string{"avc", "hevc", "vp9", "av1", "mpeg2"}, }, { // Media generation, not the Arc brand, determines support. Generic // "Intel Graphics" / "Arc Graphics" cannot distinguish these platforms. ModelNames: []string{"Core Ultra 7 155H", "Core Ultra 7 165H", "Core Ultra 9 185H", "Core Ultra 5 125H", "Arc 130T", "Arc 140T", "Arc Graphics 130T", "Arc Graphics 140T"}, Codename: "Meteor Lake", AltCodenames: []string{"MTL", "Arrow Lake", "ARL"}, Architecture: "Xe-LPG / Xe-LPG+", Codecs: []string{"avc", "hevc", "vp9", "vp8", "av1", "mpeg2"}, }, { // Only explicit V models identify Lunar Lake; "Core Ultra 200" also // includes Arrow Lake and must not grant VVC decoding. ModelNames: []string{"Core Ultra 7 258V", "Core Ultra 7 256V", "Core Ultra 7 268V", "Core Ultra 7 266V", "Core Ultra 5 228V", "Core Ultra 5 226V", "Core Ultra 5 238V", "Core Ultra 5 236V", "Core Ultra 9 288V", "Arc 130V", "Arc 140V", "Arc Graphics 130V", "Arc Graphics 140V"}, Codename: "Lunar Lake", AltCodenames: []string{"LNL"}, Architecture: "Xe2-LPG", Codecs: []string{"avc", "hevc", "vp9", "vp8", "av1", "vvc", "mpeg2"}, }, { // Intel's media-driver matrix lists no VVC for BMG. Do not match "Arc B" // broadly: B370/B390 are Panther Lake integrated GPUs, not Battlemage. ModelNames: []string{"Arc B580", "Arc B570", "Arc Pro B50", "Arc Pro B60"}, Codename: "Battlemage", AltCodenames: []string{"BMG"}, Architecture: "Xe2-HPG", Codecs: []string{"avc", "hevc", "vp9", "vp8", "av1", "mpeg2"}, }, { ModelNames: []string{"Arc B370", "Arc B390", "Arc Graphics B370", "Arc Graphics B390"}, Codename: "Panther Lake", AltCodenames: []string{"PTL"}, Architecture: "Xe3", Codecs: []string{"avc", "hevc", "vp9", "vp8", "av1", "vvc", "mpeg2"}, }, } var nvidiaGPUData = []GPUModelEntry{ { // Kepler covers the GTX 600/700 series and the original GTX TITAN cards. // Titan matching must stay GTX-qualified and lives only in SeriesNames: // the ModelNames pass runs first and claims GTX TITAN X (Maxwell) and // TITAN X/Xp/V/RTX (Pascal/Volta/Turing) for the entries below. ModelNames: []string{"GeForce GTX 600", "GeForce GTX 700", "GTX 600", "GTX 700"}, SeriesNames: []string{"GeForce GTX 600", "GeForce GTX 700", "GTX 600", "GTX 700", "GTX Titan", "GTX 650", "GTX 660", "GTX 670", "GTX 680", "GTX 690", "GTX 760", "GTX 770", "GTX 780"}, 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"}, }, { // The Maxwell Titan is branded "GeForce GTX TITAN X"; the Pascal cards // below dropped the GTX prefix ("NVIDIA TITAN X", "TITAN Xp"). ModelNames: []string{"GeForce GTX 970", "GeForce GTX 980", "GeForce Titan X", "GTX Titan X"}, SeriesNames: []string{"GeForce GTX 970", "GeForce GTX 980", "GTX 970", "GTX 980"}, 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", "TITAN X", "TITAN Xp"}, 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", "TITAN V"}, SeriesNames: []string{"NVIDIA Titan V", "Titan V"}, Codename: "GV100", Architecture: "Volta", Codecs: []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"RTX 2060", "RTX 2070", "RTX 2080", "GTX 1630", "GTX 1650", "GTX 1660", "TITAN RTX", "Quadro RTX 3000", "Quadro RTX 4000", "Quadro RTX 5000", "Quadro RTX 6000", "Quadro RTX 8000"}, Codename: "TU102", AltCodenames: []string{"TU104", "TU106", "TU116", "TU117"}, Architecture: "Turing", Codecs: []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"RTX 2050", "RTX 3050", "RTX 3060", "RTX 3070", "RTX 3080", "RTX 3090"}, Codename: "GA102", AltCodenames: []string{"GA104", "GA106", "GA107"}, Architecture: "Ampere", Codecs: []string{"avc", "hevc", "vp9", "av1", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"RTX 4050", "RTX 4060", "RTX 4070", "RTX 4080", "RTX 4090"}, Codename: "AD102", AltCodenames: []string{"AD103", "AD104", "AD106", "AD107"}, Architecture: "Ada Lovelace", Codecs: []string{"avc", "hevc", "vp9", "av1", "vp8", "mpeg2", "vc1"}, }, { ModelNames: []string{"RTX 5050", "RTX 5060", "RTX 5070", "RTX 5080", "RTX 5090"}, 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 { return detectGPULinuxWithRunner(systemProbeRunner{}) } func detectGPULinuxWithRunner(runner probeRunner) *GPUInfo { info := &GPUInfo{ Models: []string{}, } if output, err := runner.Run(newProbe("glxinfo", standardProbeTimeout, "-B")); err == nil { appendGPUModels(&info.Models, parseGlxInfo(string(output))...) } else { log.Debug("glxinfo command failed: " + err.Error()) } if output, err := runner.Run(newProbe("vulkaninfo", standardProbeTimeout, "--summary")); err == nil { appendGPUModels(&info.Models, parseVulkanInfo(string(output))...) } else { log.Debug("vulkaninfo command failed: " + err.Error()) } if output, err := runner.Run(newProbe("lspci", standardProbeTimeout, "-nn")); err == nil { appendGPUModels(&info.Models, parseLSPCIModels(string(output))...) } 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 isDRMCardName(entry.Name()) { 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) appendGPUModels(&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.Brands = make([]string, len(info.Models)) for i, model := range info.Models { info.Brands[i] = determineModelBrand(model) } for _, brand := range info.Brands { info.SupportsVA = info.SupportsVA || brand == "amd" || brand == "intel" info.SupportsNV = info.SupportsNV || brand == "nvidia" } return info } func detectGPUDarwin() *GPUInfo { return detectGPUDarwinWithRunner(systemProbeRunner{}) } func detectGPUDarwinWithRunner(runner probeRunner) *GPUInfo { info := &GPUInfo{ Models: []string{}, Brand: "apple", SupportsVT: true, } if runtime.GOARCH == "arm64" { if output, err := runner.Run(newProbe("sysctl", quickProbeTimeout, "-n", "machdep.cpu.brand_string")); err == nil { cpuModel := strings.TrimSpace(string(output)) info.Models = append(info.Models, cpuModel) return info } } if output, err := runner.Run(newProbe("system_profiler", slowProbeTimeout, "SPDisplaysDataType", "-json")); 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 { return detectGPUWindowsWithRunner(systemProbeRunner{}) } func detectGPUWindowsWithRunner(runner probeRunner) *GPUInfo { info := &GPUInfo{ Models: []string{}, } if output, err := runner.Run(newProbe("wmic", slowProbeTimeout, "path", "win32_VideoController", "get", "name")); 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 { if output, err := runner.Run(newProbe("powershell", slowProbeTimeout, "-NoProfile", "-NonInteractive", "-Command", "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name")); 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.Brands = make([]string, len(info.Models)) for i, model := range info.Models { info.Brands[i] = determineModelBrand(model) } 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 { brand := determineModelBrand(model) if brand == "" { continue } brands[brand]++ if brand == "intel" && strings.Contains(strings.ToLower(model), "arc") { hasArc = true } } 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 "" } // determineModelBrand returns the GPU vendor for a single GPU model string // ("nvidia", "amd", "intel", "apple"), or "" when it cannot be determined. // Matching is by name heuristics independent of the codec lookup table, so a // vendor is still identified for models the table doesn't know yet (e.g. an // "Arc B580" reported without the Intel(R)/Arc(TM) decoration). func determineModelBrand(model string) string { lowerModel := strings.ToLower(model) switch { case strings.Contains(lowerModel, "nvidia") || strings.Contains(lowerModel, "geforce") || strings.Contains(lowerModel, "quadro") || strings.Contains(lowerModel, "tesla") || strings.Contains(lowerModel, "gtx") || strings.Contains(lowerModel, "rtx"): return "nvidia" case strings.Contains(lowerModel, "amd") || strings.Contains(lowerModel, "radeon") || strings.Contains(lowerModel, "advanced micro devices") || strings.Contains(lowerModel, "vega") || strings.Contains(lowerModel, "rdna"): return "amd" case strings.Contains(lowerModel, "intel") || strings.Contains(lowerModel, "iris") || strings.Contains(lowerModel, "arc a") || strings.Contains(lowerModel, "arc b") || strings.Contains(lowerModel, "arc pro") || strings.Contains(lowerModel, "battlemage") || strings.Contains(lowerModel, "alchemist") || strings.Contains(lowerModel, "uhd graphics") || strings.Contains(lowerModel, "hd graphics") || strings.Contains(lowerModel, "xe graphics"): return "intel" case strings.Contains(lowerModel, "apple"): return "apple" } return "" } // GetGPUBrands returns the distinct GPU vendor brands present in info. It // prefers the per-model Brands list populated during detection, derives brands // from the model names when that list is missing (e.g. macOS), and finally // falls back to the collapsed Brand field. func GetGPUBrands(info *GPUInfo) []string { if info == nil { return nil } var brands []string if len(info.Brands) > 0 { brands = distinctBrands(info.Brands) } else { perModel := make([]string, len(info.Models)) for i, model := range info.Models { perModel[i] = determineModelBrand(model) } brands = distinctBrands(perModel) } if len(brands) == 0 { brands = distinctBrands([]string{info.Brand}) } return brands } // distinctBrands normalizes and de-duplicates GPU brand names, dropping empty entries. func distinctBrands(brands []string) []string { var distinct []string for _, brand := range brands { brand = strings.ToLower(strings.TrimSpace(brand)) if brand != "" && !contains(distinct, brand) { distinct = append(distinct, brand) } } return distinct } // multiGPUHWAFallback returns the hwdec fallback chain recommended for systems // with more than one GPU brand (e.g. Intel + NVIDIA laptops), where MPV may be // started on either GPU. It returns "" when no chain applies (macOS uses plain // "auto", which is always offered separately). func multiGPUHWAFallback(ostype OSType, brands []string) string { hasNVIDIA := false for _, b := range brands { if b == "nvidia" { hasNVIDIA = true break } } switch ostype { case Windows: if hasNVIDIA { return "nvdec-copy,vulkan-copy,auto" } return "vulkan,d3d11va,auto" case Linux: if hasNVIDIA { return "nvdec,vaapi,auto" } return "vaapi,vulkan,auto" default: // VideoToolbox works across GPUs; plain "auto" is sufficient. return "" } } // GetHWAOptions returns the hardware decoder options suitable for the given OS // and GPU brand. Additional GPU brands may be passed for multi-GPU systems // (integrated + dedicated, see GetGPUBrands); when more than one brand is // present a fallback-chain option (e.g. "nvdec-copy,vulkan-copy,auto") is // added first, since MPV may land on either GPU. func GetHWAOptions(ostype OSType, brand string, additionalBrands ...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...) } } if brands := distinctBrands(append([]string{brand}, additionalBrands...)); len(brands) > 1 { if chain := multiGPUHWAFallback(ostype, brands); chain != "" && !contains(options, chain) { options = append([]string{chain}, 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. // // Additional GPU brands may be passed for multi-GPU systems (see GetGPUBrands); // when more than one brand is present (e.g. an Intel + NVIDIA laptop) a // fallback chain is recommended instead of a single-vendor decoder, since MPV // may land on either GPU: // - Windows + Nvidia present: nvdec-copy,vulkan-copy,auto // - Windows (no Nvidia): vulkan,d3d11va,auto // - Linux + Nvidia present: nvdec,vaapi,auto // - Linux (no Nvidia): vaapi,vulkan,auto // - macOS: auto // // Platform-specific recommendations (single GPU): // - 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, additionalBrands ...string) string { brands := distinctBrands(append([]string{brand}, additionalBrands...)) if len(brands) > 1 { if chain := multiGPUHWAFallback(ostype, brands); chain != "" { return chain } return "auto" } 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 detectCodecSupportDarwinForGPU(info) default: return nil, fmt.Errorf("unsupported OS: %s", runtime.GOOS) } } func parseCodecSupportLinux(info *GPUInfo) ([]string, error) { return parseCodecSupportLinuxWithRunner(info, systemProbeRunner{}) } func parseCodecSupportLinuxWithRunner(info *GPUInfo, runner probeRunner) ([]string, error) { if output, err := runner.Run(newProbe("vainfo", standardProbeTimeout)); err == nil { if detected, err := parseVainfo(string(output)); err == nil && len(detected) > 0 { return detected, nil } } if output, err := runner.Run(newProbe("vdpauinfo", standardProbeTimeout)); err == nil { if detected, err := parseVDPAUInfo(string(output)); err == nil && len(detected) > 0 { return detected, nil } } if len(info.Models) > 0 { var combined []string for index, model := range info.Models { brand := info.Brand if index < len(info.Brands) && info.Brands[index] != "" { brand = info.Brands[index] } if codecs, matched := lookupGPUCodecsWithFallback(model, brand); matched { for _, codec := range codecs { if codec != "unknown" && !contains(combined, codec) { combined = append(combined, codec) } } } } if len(combined) > 0 { return combined, 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) { return parseCodecSupportWindowsWithRunner(info, systemProbeRunner{}) } func parseCodecSupportWindowsWithRunner(info *GPUInfo, runner probeRunner) ([]string, error) { _, err := runner.Run(newProbe("powershell", slowProbeTimeout, "-NoProfile", "-NonInteractive", "-Command", "Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name")) 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 splitPCIID(pciID string) (vendorID, deviceID string, ok bool) { pciID = strings.TrimPrefix(strings.ToLower(strings.TrimSpace(pciID)), "pci_id=") parts := strings.Split(pciID, ":") if len(parts) != 2 { return "", "", false } vendorID = strings.TrimPrefix(strings.TrimSpace(parts[0]), "0x") deviceID = strings.TrimPrefix(strings.TrimSpace(parts[1]), "0x") if len(vendorID) != 4 || len(deviceID) != 4 { return "", "", false } for _, value := range []string{vendorID, deviceID} { for _, char := range value { if !((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f')) { return "", "", false } } } return vendorID, deviceID, true } func isDRMCardName(name string) bool { if !strings.HasPrefix(name, "card") || len(name) == len("card") { return false } for _, char := range name[len("card"):] { if char < '0' || char > '9' { return false } } return true } func parsePCIIDsModel(database, vendorID, deviceID string) string { vendorID = strings.ToLower(vendorID) deviceID = strings.ToLower(deviceID) withinVendor := false vendorName := "" for _, line := range strings.Split(database, "\n") { if line == "" || strings.HasPrefix(line, "#") { continue } if !strings.HasPrefix(line, "\t") { fields := strings.Fields(line) withinVendor = len(fields) >= 2 && strings.EqualFold(fields[0], vendorID) if withinVendor { vendorName = strings.TrimSpace(strings.TrimPrefix(line, fields[0])) } continue } if !withinVendor || strings.HasPrefix(line, "\t\t") { continue } trimmed := strings.TrimSpace(line) fields := strings.Fields(trimmed) if len(fields) >= 2 && strings.EqualFold(fields[0], deviceID) { deviceName := strings.TrimSpace(strings.TrimPrefix(trimmed, fields[0])) return strings.TrimSpace(vendorName + " " + deviceName) } } return "" } func getGPUModelFromPCIID(pciID string) string { vendorID, deviceID, ok := splitPCIID(pciID) if !ok { return "" } for _, databasePath := range []string{ "/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids", "/usr/share/pci.ids", } { data, err := os.ReadFile(databasePath) if err != nil { continue } if model := parsePCIIDsModel(string(data), vendorID, deviceID); model != "" { return model } } vendorNames := map[string]string{ "1002": "AMD", "10de": "NVIDIA", "8086": "Intel", "13b5": "ARM", "5143": "Qualcomm", } vendor := vendorNames[vendorID] if vendor == "" { vendor = "Unknown" } return fmt.Sprintf("%s GPU (PCI %s:%s)", vendor, vendorID, deviceID) }