Reads the same PDH counters as Task Manager (\GPU Engine(*)\Utilization Percentage, locale-independent via PdhAddEnglishCounterW), which cover graphics work under WDDM — games are caught without an exe list and the offender is named. Windows-only; a missing or failing counter disables the path with one log line. dwm (the desktop compositor) joins the default ignore list.
112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
//go:build windows
|
|
|
|
package game
|
|
|
|
import (
|
|
"fmt"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
)
|
|
|
|
// Per-process GPU engine utilization via PDH — the same counters Task
|
|
// Manager's "GPU engine" columns read. Unlike nvidia-smi's compute-apps
|
|
// this covers graphics work under WDDM, so games show up. The counter is
|
|
// added with PdhAddEnglishCounterW, which is independent of the Windows
|
|
// display language.
|
|
|
|
var (
|
|
pdhDLL = windows.NewLazySystemDLL("pdh.dll")
|
|
procPdhOpenQuery = pdhDLL.NewProc("PdhOpenQueryW")
|
|
procPdhAddEnglishCounter = pdhDLL.NewProc("PdhAddEnglishCounterW")
|
|
procPdhCollectQueryData = pdhDLL.NewProc("PdhCollectQueryData")
|
|
procPdhGetFormattedCounterArray = pdhDLL.NewProc("PdhGetFormattedCounterArrayW")
|
|
procPdhCloseQuery = pdhDLL.NewProc("PdhCloseQuery")
|
|
)
|
|
|
|
const (
|
|
pdhFmtDouble = 0x00000200 // PDH_FMT_DOUBLE
|
|
pdhMoreData = 0x800007D2 // PDH_MORE_DATA
|
|
)
|
|
|
|
// pdhCountervalueItem mirrors PDH_FMT_COUNTERVALUE_ITEM (64-bit, double).
|
|
type pdhCountervalueItem struct {
|
|
name *uint16
|
|
cStatus uint32
|
|
_ uint32 // alignment padding
|
|
value float64
|
|
}
|
|
|
|
// gpuEngineSampler holds an open PDH query on the wildcard GPU Engine
|
|
// utilization counter. Keeping the query open across polls is what makes
|
|
// the rate-based values meaningful; the sampler lives as long as the
|
|
// process (PdhCloseQuery would only matter on unload).
|
|
type gpuEngineSampler struct {
|
|
query uintptr // PDH_HQUERY
|
|
counter uintptr // PDH_HCOUNTER
|
|
primed bool
|
|
}
|
|
|
|
// openGPUEngineSampler opens a query on the per-process GPU engine
|
|
// utilization counter (all instances).
|
|
func openGPUEngineSampler() (*gpuEngineSampler, error) {
|
|
var q uintptr
|
|
if r, _, _ := procPdhOpenQuery.Call(0, 0, uintptr(unsafe.Pointer(&q))); r != 0 {
|
|
return nil, fmt.Errorf("PdhOpenQuery: status %#x", r)
|
|
}
|
|
path, err := windows.UTF16PtrFromString(`\GPU Engine(*)\Utilization Percentage`)
|
|
if err != nil {
|
|
procPdhCloseQuery.Call(q)
|
|
return nil, err
|
|
}
|
|
var c uintptr
|
|
if r, _, _ := procPdhAddEnglishCounter.Call(q, uintptr(unsafe.Pointer(path)), 0, uintptr(unsafe.Pointer(&c))); r != 0 {
|
|
procPdhCloseQuery.Call(q)
|
|
return nil, fmt.Errorf("PdhAddEnglishCounter: status %#x", r)
|
|
}
|
|
return &gpuEngineSampler{query: q, counter: c}, nil
|
|
}
|
|
|
|
// sample collects the counter once and returns per-PID 3D-engine
|
|
// utilization in percent. The first call after open only primes the rate
|
|
// calculation and returns errNotPrimed. Processes can drive several 3D
|
|
// engines; their values are summed.
|
|
func (s *gpuEngineSampler) sample() (map[int]float64, error) {
|
|
if r, _, _ := procPdhCollectQueryData.Call(s.query); r != 0 {
|
|
return nil, fmt.Errorf("PdhCollectQueryData: status %#x", r)
|
|
}
|
|
if !s.primed {
|
|
s.primed = true
|
|
return nil, errNotPrimed
|
|
}
|
|
var size, count uint32
|
|
r, _, _ := procPdhGetFormattedCounterArray.Call(s.counter, pdhFmtDouble,
|
|
uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&count)), 0)
|
|
if r == pdhMoreData && size == 0 {
|
|
return nil, nil // no GPU engine instances at all
|
|
}
|
|
if r != pdhMoreData {
|
|
return nil, fmt.Errorf("PdhGetFormattedCounterArray(size): status %#x", r)
|
|
}
|
|
buf := make([]byte, size)
|
|
r, _, _ = procPdhGetFormattedCounterArray.Call(s.counter, pdhFmtDouble,
|
|
uintptr(unsafe.Pointer(&size)), uintptr(unsafe.Pointer(&count)),
|
|
uintptr(unsafe.Pointer(&buf[0])))
|
|
if r != 0 {
|
|
return nil, fmt.Errorf("PdhGetFormattedCounterArray: status %#x", r)
|
|
}
|
|
items := unsafe.Slice((*pdhCountervalueItem)(unsafe.Pointer(&buf[0])), int(count))
|
|
out := make(map[int]float64)
|
|
for i := range items {
|
|
if items[i].cStatus != 0 || items[i].name == nil {
|
|
continue
|
|
}
|
|
pid, engType, ok := parseGPUEngineInstance(windows.UTF16PtrToString(items[i].name))
|
|
if !ok || engType != "3D" {
|
|
continue // only the 3D engine marks game-like work
|
|
}
|
|
out[pid] += items[i].value
|
|
}
|
|
return out, nil
|
|
}
|