Files
2026-07-29 19:33:46 +08:00

327 lines
7.2 KiB
Go

//go:build windows
package gpu
import (
"log"
"os/exec"
"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 {
methods := []struct {
name string
fn func() []gpuName
}{
{"DXGI", dxgiGPUList},
{"Registry", registryGPUList},
{"PowerShell", psGPUList},
}
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
}
log.Printf("[gpu] %s returned empty", m.name)
}
return nil
}
func classifyByVID(vid uint32) string {
switch vid {
case 0x10DE:
return "nvidia"
case 0x8086, 0x1414:
return "intel"
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"
}
}
func isVirtualGPU(name string) bool {
l := toLower(name)
for _, v := range []string{
"virtual", "remote", "rdp", "citrix", "vmware", "hyper-v",
"mirror", "indirect", "parsec", "splashtop",
"mirage", "vnc", "displayonly", "basicdisplay",
"microsoft basic", "microsoft remote",
} {
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
}