package platform import ( "testing" ) // setsEqual reports whether a and b contain the same elements (order-insensitive) func setsEqual(a, b []string) bool { if len(a) != len(b) { return false } for _, item := range a { if !contains(b, item) { return false } } return true } // TestLookupGPUCodecsNvidiaTitan tests that Titan cards match the correct // architecture: Kepler Titans are GTX-prefixed, while TITAN X/Xp (Pascal), // TITAN V (Volta) and TITAN RTX (Turing) must not fall into the Kepler entry. func TestLookupGPUCodecsNvidiaTitan(t *testing.T) { kepler := []string{"avc", "mpeg2", "vc1"} maxwell := []string{"avc", "mpeg2", "vc1"} pascal := []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"} tests := []struct { name string gpu string want []string notIn []string }{ {"Pascal TITAN X parenthetical", "NVIDIA TITAN X (Pascal)", pascal, nil}, {"Pascal TITAN X", "NVIDIA TITAN X", pascal, nil}, {"Pascal TITAN Xp", "NVIDIA TITAN Xp", pascal, nil}, {"Volta TITAN V", "NVIDIA TITAN V", pascal, nil}, {"Turing TITAN RTX", "NVIDIA TITAN RTX", pascal, nil}, {"Kepler GTX TITAN", "NVIDIA GeForce GTX TITAN", kepler, []string{"hevc", "vp9"}}, {"Kepler GTX TITAN Black", "NVIDIA GeForce GTX TITAN Black", kepler, []string{"hevc", "vp9"}}, {"Kepler GTX TITAN Z", "NVIDIA GeForce GTX TITAN Z", kepler, []string{"hevc", "vp9"}}, {"Maxwell GTX TITAN X", "NVIDIA GeForce GTX TITAN X", maxwell, []string{"hevc", "vp9"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { codecs, matched := lookupGPUCodecsWithFallback(tt.gpu, "nvidia") if !matched { t.Fatalf("lookupGPUCodecsWithFallback(%q) not matched, want %v", tt.gpu, tt.want) } if !setsEqual(codecs, tt.want) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, want %v", tt.gpu, codecs, tt.want) } for _, codec := range tt.notIn { if contains(codecs, codec) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, must not contain %q", tt.gpu, codecs, codec) } } }) } } // TestLookupGPUCodecsNvidiaKepler tests real-world Kepler model strings func TestLookupGPUCodecsNvidiaKepler(t *testing.T) { kepler := []string{"avc", "mpeg2", "vc1"} tests := []struct { name string gpu string want []string }{ {"GTX 680", "NVIDIA GeForce GTX 680", kepler}, {"GTX 670", "NVIDIA GeForce GTX 670", kepler}, {"GTX 690", "NVIDIA GeForce GTX 690", kepler}, {"GTX 760", "GeForce GTX 760", kepler}, {"GTX 770", "NVIDIA GeForce GTX 770", kepler}, {"GTX 780 Ti", "NVIDIA GeForce GTX 780 Ti", kepler}, // Regression: Maxwell and Pascal cards must not match the Kepler entry {"GTX 750 Ti is Maxwell", "NVIDIA GeForce GTX 750 Ti", kepler}, {"GTX 1080 Ti is Pascal", "NVIDIA GeForce GTX 1080 Ti", []string{"avc", "hevc", "vp9", "vp8", "mpeg2", "vc1"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { codecs, matched := lookupGPUCodecsWithFallback(tt.gpu, "nvidia") if !matched { t.Fatalf("lookupGPUCodecsWithFallback(%q) not matched, want %v", tt.gpu, tt.want) } if !setsEqual(codecs, tt.want) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, want %v", tt.gpu, codecs, tt.want) } }) } } // TestLookupGPUCodecsAMDAPU tests the APU iGPU entries (issue #7) func TestLookupGPUCodecsAMDAPU(t *testing.T) { rdna2APU := []string{"avc", "hevc", "vp9", "av1"} rdna3APU := []string{"avc", "hevc", "vp9", "av1"} vegaAPU := []string{"avc", "hevc", "vp9"} tests := []struct { name string gpu string want []string }{ {"680M plain", "AMD Radeon 680M", rdna2APU}, {"680M trademark form", "Radeon(TM) 680M", rdna2APU}, {"780M", "AMD Radeon(TM) 780M", rdna3APU}, {"780M plain", "AMD Radeon 780M", rdna3APU}, {"890M", "AMD Radeon(TM) 890M", rdna3APU}, {"Vega 8 full string", "AMD Radeon(TM) Vega 8 Graphics", vegaAPU}, {"Vega 8 bare", "Vega 8", vegaAPU}, {"Vega 6", "AMD Radeon(TM) Vega 6 Graphics", vegaAPU}, {"Vega 11", "AMD Radeon Vega 11", vegaAPU}, // Regression: discrete cards must not match the APU entries {"RX Vega 64 stays discrete", "Radeon RX Vega 64", []string{"avc", "hevc", "vp9", "vc1", "mpeg2"}}, {"RX 6800 XT stays Navi 21", "AMD Radeon RX 6800 XT", []string{"avc", "hevc", "vp9", "av1", "vc1", "mpeg2"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { codecs, matched := lookupGPUCodecsWithFallback(tt.gpu, "amd") if !matched { t.Fatalf("lookupGPUCodecsWithFallback(%q) not matched, want %v", tt.gpu, tt.want) } if !setsEqual(codecs, tt.want) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, want %v", tt.gpu, codecs, tt.want) } }) } } // TestLookupGPUCodecsIntelCoreUltra tests the Meteor Lake / Lunar Lake split: // Meteor Lake (Core Ultra 100) has no VVC decoder, Lunar Lake (Core Ultra // 200V) does. func TestLookupGPUCodecsIntelCoreUltra(t *testing.T) { meteor := []string{"avc", "hevc", "vp9", "vp8", "av1", "mpeg2"} lunar := []string{"avc", "hevc", "vp9", "vp8", "av1", "vvc", "mpeg2"} tests := []struct { name string gpu string want []string notIn []string }{ {"Meteor Lake Core Ultra 7 155H", "Core Ultra 7 155H", meteor, []string{"vvc"}}, {"Meteor Lake Core Ultra 9 185H", "Intel Core Ultra 9 185H", meteor, []string{"vvc"}}, {"Meteor Lake Core Ultra 5 125H", "Core Ultra 5 125H", meteor, []string{"vvc"}}, {"Lunar Lake Core Ultra 7 258V", "Core Ultra 7 258V", lunar, nil}, {"Lunar Lake by name", "Lunar Lake", lunar, nil}, // Regression: Arc Alchemist unaffected {"Arc A770", "Intel Arc A770", []string{"avc", "hevc", "vp9", "av1", "mpeg2"}, []string{"vvc", "vp8"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { codecs, matched := lookupGPUCodecsWithFallback(tt.gpu, "intel") if !matched { t.Fatalf("lookupGPUCodecsWithFallback(%q) not matched, want %v", tt.gpu, tt.want) } if !setsEqual(codecs, tt.want) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, want %v", tt.gpu, codecs, tt.want) } for _, codec := range tt.notIn { if contains(codecs, codec) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, must not contain %q", tt.gpu, codecs, codec) } } }) } } // TestDetermineModelBrand tests per-model brand detection func TestDetermineModelBrand(t *testing.T) { tests := []struct { model string want string }{ {"NVIDIA GeForce RTX 3080", "nvidia"}, {"GeForce GTX 1080", "nvidia"}, {"AMD Radeon(TM) 780M", "amd"}, {"Radeon RX 6800 XT", "amd"}, {"Intel UHD Graphics 770", "intel"}, {"Intel Arc A380", "intel"}, {"Apple M4", "apple"}, {"Microsoft Basic Render Driver", ""}, } for _, tt := range tests { t.Run(tt.model, func(t *testing.T) { if got := determineModelBrand(tt.model); got != tt.want { t.Errorf("determineModelBrand(%q) = %q, want %q", tt.model, got, tt.want) } }) } } // TestGetGPUBrands tests distinct brand collection from GPUInfo func TestGetGPUBrands(t *testing.T) { tests := []struct { name string info *GPUInfo want []string }{ {"nil info", nil, nil}, {"from Brands", &GPUInfo{Brands: []string{"intel", "nvidia"}, Brand: "nvidia"}, []string{"intel", "nvidia"}}, {"derived from Models", &GPUInfo{Models: []string{"Intel UHD Graphics 770", "NVIDIA GeForce RTX 3060"}}, []string{"intel", "nvidia"}}, {"single apple", &GPUInfo{Models: []string{"Apple M4"}}, []string{"apple"}}, {"fallback to Brand", &GPUInfo{Brand: "amd"}, []string{"amd"}}, {"empty brand entries dropped", &GPUInfo{Brands: []string{"", "nvidia"}, Brand: "nvidia"}, []string{"nvidia"}}, {"duplicates removed", &GPUInfo{Brands: []string{"nvidia", "NVIDIA"}, Brand: "nvidia"}, []string{"nvidia"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := GetGPUBrands(tt.info) if len(got) == 0 && len(tt.want) == 0 { return } if !setsEqual(got, tt.want) { t.Errorf("GetGPUBrands() = %v, want %v", got, tt.want) } }) } } // TestGetRecommendedHWADecoderMultiBrand tests that multi-GPU systems get a // fallback chain while single-GPU behavior is unchanged func TestGetRecommendedHWADecoderMultiBrand(t *testing.T) { tests := []struct { name string ostype OSType brand string extra []string want string }{ // Multi-brand systems get the fallback chain {"Windows intel+nvidia", Windows, "intel", []string{"nvidia"}, "nvdec-copy,vulkan-copy,auto"}, {"Windows nvidia+intel order-independent", Windows, "nvidia", []string{"intel"}, "nvdec-copy,vulkan-copy,auto"}, {"Linux intel+nvidia", Linux, "intel", []string{"nvidia"}, "nvdec,vaapi,auto"}, {"Windows amd+intel", Windows, "amd", []string{"intel"}, "vulkan,d3d11va,auto"}, {"Linux amd+intel", Linux, "amd", []string{"intel"}, "vaapi,vulkan,auto"}, {"Darwin multi stays auto", Darwin, "intel", []string{"amd"}, "auto"}, // Single-brand behavior unchanged {"Windows nvidia", Windows, "nvidia", nil, "nvdec-copy,auto"}, {"Windows amd", Windows, "amd", nil, "vulkan,auto"}, {"Windows intel", Windows, "intel", nil, "vulkan,auto"}, {"Windows unknown", Windows, "", nil, "auto"}, {"Linux nvidia", Linux, "nvidia", nil, "nvdec,auto"}, {"Linux amd", Linux, "amd", nil, "auto"}, {"Linux intel", Linux, "intel", nil, "auto"}, {"Darwin apple", Darwin, "apple", nil, "auto"}, {"Darwin intel", Darwin, "intel", nil, "auto"}, // Duplicate brands collapse to single-brand behavior {"duplicate nvidia", Windows, "nvidia", []string{"nvidia"}, "nvdec-copy,auto"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := GetRecommendedHWADecoder(tt.ostype, tt.brand, tt.extra...); got != tt.want { t.Errorf("GetRecommendedHWADecoder(%v, %q, %v) = %q, want %q", tt.ostype, tt.brand, tt.extra, got, tt.want) } }) } } // TestGetHWAOptionsMultiBrand tests that multi-GPU systems get the fallback // chain option prepended while existing single options are kept func TestGetHWAOptionsMultiBrand(t *testing.T) { t.Run("Windows intel+nvidia chain first", func(t *testing.T) { options := GetHWAOptions(Windows, "intel", "nvidia") if len(options) == 0 || options[0] != "nvdec-copy,vulkan-copy,auto" { t.Errorf("GetHWAOptions(Windows, intel, nvidia)[0] = %v, want nvdec-copy,vulkan-copy,auto first", options) } if !contains(options, "d3d11va") { t.Errorf("GetHWAOptions(Windows, intel, nvidia) = %v, want d3d11va kept", options) } }) t.Run("Windows nvidia+intel keeps nvdec", func(t *testing.T) { options := GetHWAOptions(Windows, "nvidia", "intel") if len(options) == 0 || options[0] != "nvdec-copy,vulkan-copy,auto" { t.Errorf("GetHWAOptions(Windows, nvidia, intel)[0] = %v, want nvdec-copy,vulkan-copy,auto first", options) } if !contains(options, "nvdec") { t.Errorf("GetHWAOptions(Windows, nvidia, intel) = %v, want nvdec kept", options) } }) t.Run("Linux intel+nvidia chain first", func(t *testing.T) { options := GetHWAOptions(Linux, "intel", "nvidia") if len(options) == 0 || options[0] != "nvdec,vaapi,auto" { t.Errorf("GetHWAOptions(Linux, intel, nvidia)[0] = %v, want nvdec,vaapi,auto first", options) } }) t.Run("Linux amd+intel chain first", func(t *testing.T) { options := GetHWAOptions(Linux, "amd", "intel") if len(options) == 0 || options[0] != "vaapi,vulkan,auto" { t.Errorf("GetHWAOptions(Linux, amd, intel)[0] = %v, want vaapi,vulkan,auto first", options) } }) t.Run("single brand gets no chain", func(t *testing.T) { if options := GetHWAOptions(Windows, "nvidia"); contains(options, "nvdec-copy,vulkan-copy,auto") { t.Errorf("GetHWAOptions(Windows, nvidia) = %v, must not contain multi-GPU chain", options) } if options := GetHWAOptions(Linux, "nvidia"); contains(options, "nvdec,vaapi,auto") { t.Errorf("GetHWAOptions(Linux, nvidia) = %v, must not contain multi-GPU chain", options) } if options := GetHWAOptions(Linux, "amd"); contains(options, "vaapi,vulkan,auto") { t.Errorf("GetHWAOptions(Linux, amd) = %v, must not contain multi-GPU chain", options) } }) t.Run("Darwin multi gets no chain", func(t *testing.T) { options := GetHWAOptions(Darwin, "apple", "amd") if len(options) != 2 || !contains(options, "videotoolbox") || !contains(options, "videotoolbox-copy") { t.Errorf("GetHWAOptions(Darwin, apple, amd) = %v, want [videotoolbox videotoolbox-copy]", options) } }) } // TestDetermineGPUBrandArcOnly is a regression test for the bug where a system // whose only GPU is an Intel Arc (e.g. Arc B580) got an empty vendor: the old // code set hasArc for "intel"+"arc" models but never counted the brand, so the // brands map stayed empty and determineGPUBrand returned "". func TestDetermineGPUBrandArcOnly(t *testing.T) { tests := []struct { name string models []string want string }{ {"arc only (reported bug)", []string{"Intel(R) Arc(TM) B580 Graphics"}, "intel"}, {"arc only, bare name", []string{"Arc B580"}, "intel"}, {"arc only, no decoration", []string{"Intel Arc A770"}, "intel"}, {"intel iGPU only", []string{"Intel UHD Graphics 770"}, "intel"}, {"amd iGPU + arc dGPU prefers arc", []string{"AMD Radeon(TM) 780M", "Intel(R) Arc(TM) B580 Graphics"}, "intel"}, {"amd iGPU + intel iGPU prefers amd", []string{"AMD Radeon(TM) 780M", "Intel UHD Graphics 770"}, "amd"}, {"intel iGPU + nvidia dGPU prefers nvidia", []string{"Intel UHD Graphics 770", "NVIDIA GeForce RTX 3060"}, "nvidia"}, {"amd + nvidia prefers nvidia", []string{"AMD Radeon RX 6800 XT", "NVIDIA GeForce RTX 3060"}, "nvidia"}, {"empty", nil, ""}, {"unrecognized only", []string{"llvmpipe (LLVM 15.0, 256 bits)"}, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := determineGPUBrand(tt.models); got != tt.want { t.Errorf("determineGPUBrand(%v) = %q, want %q", tt.models, got, tt.want) } }) } } // TestDetermineModelBrandFallbacks tests vendor detection from model names that // lack the vendor trademark (the model-table lookup must not be required for // vendor identification). func TestDetermineModelBrandFallbacks(t *testing.T) { tests := []struct { model string want string }{ {"Arc B580", "intel"}, {"Arc A750 Limited Edition", "intel"}, {"Arc Pro B60", "intel"}, {"Battlemage G21", "intel"}, {"Alchemist DG2", "intel"}, {"Iris Xe Graphics", "intel"}, {"GTX 1080 Ti", "nvidia"}, {"RTX 5090", "nvidia"}, {"Vega 8", "amd"}, {"Microsoft Basic Render Driver", ""}, {"llvmpipe (LLVM 15.0, 256 bits)", ""}, {"virtio-gpu", ""}, } for _, tt := range tests { t.Run(tt.model, func(t *testing.T) { if got := determineModelBrand(tt.model); got != tt.want { t.Errorf("determineModelBrand(%q) = %q, want %q", tt.model, got, tt.want) } }) } } // TestLookupGPUCodecsBattlemage verifies the Intel Arc B-Series (Xe2-HPG) // codec entry. Unlike Lunar Lake, Battlemage has no VVC decode in Intel's matrix. func TestLookupGPUCodecsBattlemage(t *testing.T) { tests := []struct { gpu string want []string }{ {"Intel(R) Arc(TM) B580 Graphics", []string{"avc", "hevc", "vp9", "vp8", "av1", "mpeg2"}}, {"Arc B570", []string{"avc", "hevc", "vp9", "vp8", "av1", "mpeg2"}}, } for _, tt := range tests { t.Run(tt.gpu, func(t *testing.T) { codecs, matched := lookupGPUCodecsWithFallback(tt.gpu, "intel") if !matched { t.Fatalf("lookupGPUCodecsWithFallback(%q) not matched, want %v", tt.gpu, tt.want) } for _, codec := range tt.want { if !contains(codecs, codec) { t.Errorf("lookupGPUCodecsWithFallback(%q) = %v, missing %q", tt.gpu, codecs, codec) } } }) } }