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