1. 增加配置持久化

2. 完善细节
This commit is contained in:
sansen
2026-07-29 19:33:46 +08:00
parent 86532cc2e9
commit a53e205bfb
12 changed files with 721 additions and 215 deletions
+84 -55
View File
@@ -5,14 +5,12 @@ import (
"log"
)
// GPUInfo holds detected GPU information.
type GPUInfo struct {
Name string `json:"name"`
Vendor string `json:"vendor"`
Encoders []EncoderCap `json:"encoders"`
}
// EncoderCap describes an encoder capability.
type EncoderCap struct {
Name string `json:"name"`
Codec string `json:"codec"`
@@ -20,10 +18,7 @@ type EncoderCap struct {
Available bool `json:"available"`
}
// knownEncoders defines all known hardware encoders by vendor.
type knownEnc struct {
Name, Vendor, Codec, Label string
}
type knownEnc struct{ Name, Vendor, Codec, Label string }
var allEncoders = []knownEnc{
{"h264_nvenc", "nvidia", "h264", "H.264 NVENC"},
@@ -37,72 +32,111 @@ var allEncoders = []knownEnc{
{"av1_amf", "amd", "av1", "AV1 AMF"},
}
// DetectGPUs detects GPUs and their encoder capabilities.
// Uses a single ffmpeg -encoders call for speed.
var cpuEncoders = []knownEnc{
{"libx264", "cpu", "h264", "H.264 (libx264)"},
{"libx265", "cpu", "hevc", "H.265/HEVC (libx265)"},
{"libsvtav1", "cpu", "av1", "AV1 (libsvtav1)"},
}
func DetectGPUs(exec *ffmpeg.Executor) []GPUInfo {
encList, err := exec.RunSync("-encoders")
if err != nil {
log.Printf("[gpu] ffmpeg -encoders failed: %v", err)
return fallbackGPUs()
}
// Group encoders by vendor
vendorEncs := map[string][]EncoderCap{}
for _, ke := range allEncoders {
available := containsWord(encList, ke.Name)
vendorEncs[ke.Vendor] = append(vendorEncs[ke.Vendor], EncoderCap{
Name: ke.Name, Codec: ke.Codec, Label: ke.Label, Available: available,
})
}
// Get GPU names
// Step 1: Get GPU names (DXGI/Registry/PS — no ffmpeg needed)
gpuNames := detectGPUNames()
log.Printf("[gpu] detected GPU names: %v", gpuNames)
log.Printf("[gpu] names: %v", gpuNames)
// 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)
}
}
// Fall back to ffmpeg if no GPU names were found
if len(gpuNames) == 0 && ffmpegOK {
gpuNames = ffmpegFallback(exec)
}
if len(gpuNames) == 0 {
gpuNames = []gpuName{{Name: "Unknown GPU", Vendor: "unknown"}}
}
// Build result
var result []GPUInfo
for _, gn := range gpuNames {
encs := vendorEncs[gn.Vendor]
if encs == nil {
encs = []EncoderCap{}
}
result = append(result, GPUInfo{
Name: gn.Name,
Vendor: gn.Vendor,
Encoders: encs,
Encoders: capsForVendor(encList, gn.Vendor, ffmpegOK),
})
}
// Add CPU entry
// CPU
cpuName := detectCPUName()
log.Printf("[gpu] CPU: %s", cpuName)
result = append(result, GPUInfo{
Name: "CPU",
Vendor: "cpu",
Encoders: []EncoderCap{
{Name: "libx264", Codec: "h264", Label: "H.264 (libx264)", Available: containsWord(encList, "libx264")},
{Name: "libx265", Codec: "hevc", Label: "H.265/HEVC (libx265)", Available: containsWord(encList, "libx265")},
{Name: "libsvtav1", Codec: "av1", Label: "AV1 (libsvtav1)", Available: containsWord(encList, "libsvtav1")},
},
Name: cpuName,
Vendor: "cpu",
Encoders: capsForVendor(encList, "cpu", ffmpegOK),
})
log.Printf("[gpu] returning %d GPU entries", len(result))
return result
}
func containsWord(text, word string) bool {
// Simple substring match — the encoder list has one encoder per line
return len(text) > 0 && len(word) > 0 && containsLine(text, word)
func 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
}
func containsLine(text, word string) bool {
for i := 0; i < len(text); i++ {
if i+len(word) > len(text) {
break
}
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++ {
if text[i:i+len(word)] == word {
// Check word boundaries: should be preceded by space/newline and followed by space/newline
before := i == 0 || text[i-1] == ' ' || text[i-1] == '\n' || text[i-1] == '\r'
after := i+len(word) >= len(text) || text[i+len(word)] == ' ' || text[i+len(word)] == '\n' || text[i+len(word)] == '\r'
if before && after {
@@ -113,11 +147,6 @@ func containsLine(text, word string) bool {
return false
}
func fallbackGPUs() []GPUInfo {
return []GPUInfo{
{Name: "CPU", Vendor: "cpu", Encoders: []EncoderCap{
{Name: "libx264", Codec: "h264", Label: "H.264 (libx264)", Available: true},
{Name: "libx265", Codec: "hevc", Label: "H.265/HEVC (libx265)", Available: true},
}},
}
func wordIn(text, word string) bool {
return containsWord(text, word)
}
+2
View File
@@ -10,3 +10,5 @@ type gpuName struct {
func detectGPUNames() []gpuName {
return []gpuName{{Name: "Default GPU", Vendor: "unknown"}}
}
func detectCPUName() string { return "CPU" }
+288 -126
View File
@@ -5,141 +5,231 @@ package gpu
import (
"log"
"os/exec"
"strings"
"syscall"
"unsafe"
)
var hideWindow = &syscall.SysProcAttr{HideWindow: true}
type gpuName struct {
Name string
Vendor string
}
// --- Method 1: DXGI ---
var (
dxgi = syscall.NewLazyDLL("dxgi.dll")
createDXGIFactory1 = dxgi.NewProc("CreateDXGIFactory1")
IID_IDXGIFactory1 = &syscall.GUID{0x770aae78, 0xf26f, 0x4dba, [8]byte{0xa8, 0x29, 0x25, 0x3c, 0x83, 0xd1, 0xb3, 0x87}}
)
type dxgiDesc struct {
Description [128]uint16
VendorId uint32
DeviceId uint32
SubSysId uint32
Revision uint32
DedicatedVideoMemory uint64
_ uint64
_ uint64
_ [8]byte
}
func dxgiGPUList() []gpuName {
var factory uintptr
r, _, _ := createDXGIFactory1.Call(uintptr(unsafe.Pointer(IID_IDXGIFactory1)), uintptr(unsafe.Pointer(&factory)))
if r != 0 {
return nil
}
defer release(factory)
var gpus []gpuName
for i := uint32(0); ; i++ {
var adapter uintptr
fn := callVtable(factory, 12) // EnumAdapters1
r, _, _ = syscall.SyscallN(fn, factory, uintptr(i), uintptr(unsafe.Pointer(&adapter)))
if r != 0 {
break
}
var desc dxgiDesc
syscall.SyscallN(callVtable(adapter, 8), adapter, uintptr(unsafe.Pointer(&desc))) // GetDesc
release(adapter)
name := syscall.UTF16ToString(desc.Description[:])
vendor := classifyByVID(desc.VendorId)
log.Printf("[gpu][dxgi] %d: %s (VID=0x%04x)", i, name, desc.VendorId)
if name == "" || isVirtualGPU(name) || vendor == "unknown" {
continue
}
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
}
return gpus
}
func callVtable(obj uintptr, idx int) uintptr {
vtablePtr := *(**uintptr)(unsafe.Pointer(obj))
arr := (*[64]uintptr)(unsafe.Pointer(vtablePtr))
return arr[idx]
}
func release(obj uintptr) { syscall.SyscallN(callVtable(obj, 2), obj) }
// --- Method 2: Registry ---
func registryGPUList() []gpuName {
cmd := exec.Command("reg", "query",
`HKLM\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}`,
"/s", "/v", "DriverDesc")
cmd.SysProcAttr = hideWindow
out, err := cmd.CombinedOutput()
if err != nil {
return nil
}
return parseRegOutput(string(out))
}
func parseRegOutput(out string) []gpuName {
var gpus []gpuName
seen := map[string]bool{}
for _, line := range splitLines(out) {
line = trim(line)
if !startsWith(line, "DriverDesc") {
continue
}
// Format: " DriverDesc REG_SZ NVIDIA GeForce RTX 4090"
parts := splitN(line, "REG_SZ", 2)
if len(parts) < 2 {
continue
}
name := trim(parts[1])
if name == "" || seen[name] || isVirtualGPU(name) {
continue
}
vendor := classifyByName(name)
if vendor == "unknown" {
log.Printf("[gpu][reg] unknown vendor: %s", name)
continue
}
seen[name] = true
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
}
return gpus
}
// --- Method 3: PowerShell ---
func psGPUList() []gpuName {
cmd := exec.Command("powershell", "-NoProfile", "-Command",
`Get-CimInstance Win32_VideoController | ForEach-Object { $_.Name + "|" + $_.AdapterCompatibility }`,
)
cmd.SysProcAttr = hideWindow
out, err := cmd.CombinedOutput()
if err != nil {
return nil
}
var gpus []gpuName
seen := map[string]bool{}
for _, line := range splitLines(string(out)) {
line = trim(line)
if line == "" {
continue
}
parts := splitN(line, "|", 2)
name := trim(parts[0])
vendorStr := ""
if len(parts) > 1 {
vendorStr = trim(parts[1])
}
if name == "" || seen[name] || isVirtualGPU(name) {
continue
}
vendor := classifyByName(vendorStr + " " + name)
if vendor == "unknown" {
continue
}
seen[name] = true
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
}
return gpus
}
// --- CPU Detection ---
func detectCPUName() string {
cmd := exec.Command("reg", "query",
`HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor\0`,
"/v", "ProcessorNameString")
cmd.SysProcAttr = hideWindow
out, err := cmd.CombinedOutput()
if err != nil {
return detectCPUByEnv()
}
for _, line := range splitLines(string(out)) {
line = trim(line)
if !startsWith(line, "ProcessorNameString") {
continue
}
parts := splitN(line, "REG_SZ", 2)
if len(parts) >= 2 {
return trim(parts[1])
}
}
return detectCPUByEnv()
}
func detectCPUByEnv() string {
cmd := exec.Command("cmd", "/c", "echo %PROCESSOR_IDENTIFIER%")
cmd.SysProcAttr = hideWindow
out, err := cmd.CombinedOutput()
if err != nil {
return "CPU"
}
name := trim(string(out))
if name == "" {
return "CPU"
}
return name
}
// --- Orchestrator ---
func detectGPUNames() []gpuName {
// Try PowerShell first, then wmic
gpus := psGPUList()
if len(gpus) > 0 {
log.Printf("[gpu] PowerShell found %d GPUs", len(gpus))
for _, g := range gpus {
log.Printf("[gpu] %s -> vendor=%s", g.Name, g.Vendor)
}
return gpus
methods := []struct {
name string
fn func() []gpuName
}{
{"DXGI", dxgiGPUList},
{"Registry", registryGPUList},
{"PowerShell", psGPUList},
}
gpus = wmicGPUList()
if len(gpus) > 0 {
log.Printf("[gpu] WMIC found %d GPUs", len(gpus))
for _, g := range gpus {
log.Printf("[gpu] %s -> vendor=%s", g.Name, g.Vendor)
for _, m := range methods {
gpus := m.fn()
if len(gpus) > 0 {
log.Printf("[gpu] %s detected %d GPU(s)", m.name, len(gpus))
return gpus
}
return gpus
log.Printf("[gpu] %s returned empty", m.name)
}
log.Printf("[gpu] No GPUs detected via PowerShell or WMIC")
return nil
}
func psGPUList() []gpuName {
out, err := exec.Command("powershell", "-NoProfile", "-Command",
"Get-CimInstance Win32_VideoController | Select-Object Name,AdapterCompatibility,DriverVersion | ConvertTo-Csv -NoTypeInformation",
).CombinedOutput()
log.Printf("[gpu] PS output:\n%s", string(out))
if err != nil {
log.Printf("[gpu] PS error: %v", err)
return nil
}
return parseCSV(string(out))
}
func wmicGPUList() []gpuName {
out, err := exec.Command("wmic", "path", "win32_VideoController",
"get", "Name,AdapterCompatibility", "/format:csv").CombinedOutput()
log.Printf("[gpu] WMIC output:\n%s", string(out))
if err != nil {
log.Printf("[gpu] WMIC error: %v", err)
return nil
}
return parseWMIC(string(out))
}
func parseCSV(out string) []gpuName {
var gpus []gpuName
seen := map[string]bool{}
lines := strings.Split(out, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "\"Name\"") || strings.HasPrefix(line, "Name") {
continue
}
// Remove quotes: "Name","AdapterCompatibility"
line = strings.ReplaceAll(line, "\"", "")
parts := strings.SplitN(line, ",", 2)
if len(parts) < 1 {
continue
}
name := strings.TrimSpace(parts[0])
vendor := ""
if len(parts) >= 2 {
vendor = strings.TrimSpace(parts[1])
}
if name == "" || seen[name] {
continue
}
if isVirtualGPU(name) {
log.Printf("[gpu] SKIP virtual: %s", name)
continue
}
vendor = classifyVendor(vendor, name)
if vendor == "unknown" {
log.Printf("[gpu] SKIP unknown vendor (vendor=%q name=%q)", vendor, name)
continue
}
seen[name] = true
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
}
return gpus
}
func parseWMIC(out string) []gpuName {
var gpus []gpuName
seen := map[string]bool{}
lines := strings.Split(out, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "Node,") {
continue
}
parts := strings.SplitN(line, ",", 3)
if len(parts) < 3 {
continue
}
name := strings.TrimSpace(parts[2])
vendor := strings.TrimSpace(parts[1])
if name == "" || seen[name] {
continue
}
if isVirtualGPU(name) {
log.Printf("[gpu] SKIP virtual: %s", name)
continue
}
vendor = classifyVendor(vendor, name)
if vendor == "unknown" {
log.Printf("[gpu] SKIP unknown vendor (vendor=%q name=%q)", vendor, name)
continue
}
seen[name] = true
gpus = append(gpus, gpuName{Name: name, Vendor: vendor})
}
return gpus
}
func classifyVendor(vendor, name string) string {
lower := strings.ToLower(vendor + " " + name)
switch {
case strings.Contains(lower, "nvidia"):
func classifyByVID(vid uint32) string {
switch vid {
case 0x10DE:
return "nvidia"
case strings.Contains(lower, "intel") || strings.Contains(lower, "uhd graphics") ||
strings.Contains(lower, "iris") || strings.Contains(lower, "hd graphics"):
case 0x8086, 0x1414:
return "intel"
case strings.Contains(lower, "amd") || strings.Contains(lower, "radeon") || strings.Contains(lower, "ati"):
case 0x1002:
return "amd"
default:
return "unknown"
}
}
func classifyByName(name string) string {
l := toLower(name)
switch {
case strContains(l, "nvidia"):
return "nvidia"
case strContains(l, "intel") || strContains(l, "uhd") || strContains(l, "iris") || strContains(l, "hd graphics") || strContains(l, "arc"):
return "intel"
case strContains(l, "amd") || strContains(l, "radeon") || strContains(l, "ati") || strContains(l, "firepro"):
return "amd"
default:
return "unknown"
@@ -147,18 +237,90 @@ func classifyVendor(vendor, name string) string {
}
func isVirtualGPU(name string) bool {
lower := strings.ToLower(name)
// Filter out virtual/remote/display-only adapters that don't have encoders
virtual := []string{
l := toLower(name)
for _, v := range []string{
"virtual", "remote", "rdp", "citrix", "vmware", "hyper-v",
"mirror", "indirect", "parsec", "splashtop", "idm",
"mirror", "indirect", "parsec", "splashtop",
"mirage", "vnc", "displayonly", "basicdisplay",
"microsoft basic", "microsoft remote",
}
for _, v := range virtual {
if strings.Contains(lower, v) {
} {
if strContains(l, v) {
return true
}
}
return false
}
func toLower(s string) string {
b := make([]byte, len(s))
for i := 0; i < len(s); i++ {
c := s[i]
if c >= 'A' && c <= 'Z' {
c += 32
}
b[i] = c
}
return string(b)
}
func strContains(s, sub string) bool {
return len(sub) <= len(s) && indexOf(s, sub) >= 0
}
func indexOf(s, sub string) int {
for i := 0; i <= len(s)-len(sub); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func splitLines(s string) []string {
var lines []string
for {
i := indexOf(s, "\n")
if i < 0 {
if s != "" {
lines = append(lines, s)
}
break
}
if i > 0 && s[i-1] == '\r' {
lines = append(lines, s[:i-1])
} else {
lines = append(lines, s[:i])
}
s = s[i+1:]
}
return lines
}
func trim(s string) string {
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\r' || s[0] == '\n') {
s = s[1:]
}
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t' || s[len(s)-1] == '\r' || s[len(s)-1] == '\n') {
s = s[:len(s)-1]
}
return s
}
func startsWith(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
func splitN(s, sep string, n int) []string {
var r []string
for n > 1 {
i := indexOf(s, sep)
if i < 0 {
break
}
r = append(r, s[:i])
s = s[i+len(sep):]
n--
}
r = append(r, s)
return r
}