fix: parse vulkan VRAM from text (#9669)

* fix: parse vulkan VRAM from text

Assisted-by: opencode:gpt-5.5
Signed-off-by: Andreas Egli <github@kharan.ch>

* fix: replace string.split with streaming iteration

Assisted-by: Opencode:Gemma4
Signed-off-by: Andreas Egli <github@kharan.ch>

---------

Signed-off-by: Andreas Egli <github@kharan.ch>
This commit is contained in:
Andreas Egli
2026-05-12 09:53:48 +02:00
committed by GitHub
parent 37991c8a18
commit 03815e3b59

View File

@@ -1,8 +1,10 @@
package xsysinfo
import (
"bufio"
"bytes"
"encoding/json"
"io"
"os"
"os/exec"
"strconv"
@@ -801,14 +803,15 @@ func GetResourceAggregateInfo() AggregateMemoryInfo {
return resourceInfo.Aggregate
}
// getVulkanGPUMemory queries GPUs using vulkaninfo as a fallback
// Note: Vulkan provides memory heap info but not real-time usage
// getVulkanGPUMemory queries GPUs using vulkaninfo as a fallback.
// Note: vulkaninfo JSON is a Vulkan Profiles export and does not include
// VkPhysicalDeviceMemoryProperties, so memory heaps are parsed from text output.
func getVulkanGPUMemory() []GPUMemoryInfo {
if _, err := exec.LookPath("vulkaninfo"); err != nil {
return nil
}
cmd := exec.Command("vulkaninfo", "--json")
cmd := exec.Command("vulkaninfo", "--text")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
@@ -819,60 +822,174 @@ func getVulkanGPUMemory() []GPUMemoryInfo {
return nil
}
// Parse Vulkan JSON output
var result struct {
VkPhysicalDevices []struct {
DeviceName string `json:"deviceName"`
DeviceType string `json:"deviceType"`
VkPhysicalDeviceMemoryProperties struct {
MemoryHeaps []struct {
Flags int `json:"flags"`
Size uint64 `json:"size"`
} `json:"memoryHeaps"`
} `json:"VkPhysicalDeviceMemoryProperties"`
} `json:"VkPhysicalDevices"`
}
return parseVulkanGPUMemoryText(strings.NewReader(stdout.String()))
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
xlog.Debug("failed to parse vulkaninfo output", "error", err)
return nil
}
}
type vulkanGPUTextInfo struct {
index int
name string
deviceType string
totalVRAM uint64
}
func parseVulkanGPUMemoryText(r io.Reader) []GPUMemoryInfo {
var gpus []GPUMemoryInfo
var current *vulkanGPUTextInfo
for i, device := range result.VkPhysicalDevices {
// Skip non-discrete/integrated GPUs if possible
if device.DeviceType == "VK_PHYSICAL_DEVICE_TYPE_CPU" {
continue
inMemoryProperties := false
inMemoryHeaps := false
inHeap := false
heapSize := uint64(0)
heapDeviceLocal := false
flushHeap := func() {
if current != nil && inHeap && heapDeviceLocal {
current.totalVRAM += heapSize
}
heapSize = 0
heapDeviceLocal = false
inHeap = false
}
// Sum up device-local memory heaps
var totalVRAM uint64
for _, heap := range device.VkPhysicalDeviceMemoryProperties.MemoryHeaps {
// Flag 1 = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT
if heap.Flags&1 != 0 {
totalVRAM += heap.Size
}
}
if totalVRAM == 0 {
continue
flushGPU := func() {
if current == nil || current.totalVRAM == 0 || current.deviceType == "PHYSICAL_DEVICE_TYPE_CPU" {
return
}
gpus = append(gpus, GPUMemoryInfo{
Index: i,
Name: device.DeviceName,
Index: current.index,
Name: current.name,
Vendor: VendorVulkan,
TotalVRAM: totalVRAM,
UsedVRAM: 0, // Vulkan doesn't provide real-time usage
FreeVRAM: totalVRAM,
TotalVRAM: current.totalVRAM,
UsedVRAM: 0, // Vulkan heap size is capacity, not real-time usage.
FreeVRAM: current.totalVRAM,
UsagePercent: 0,
})
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
if index, ok := parseVulkanGPUHeader(line); ok {
flushHeap()
flushGPU()
current = &vulkanGPUTextInfo{index: index}
inMemoryProperties = false
inMemoryHeaps = false
continue
}
if current == nil {
continue
}
if strings.HasPrefix(line, "deviceType") {
current.deviceType = parseVulkanValue(line)
continue
}
if strings.HasPrefix(line, "deviceName") {
current.name = parseVulkanValue(line)
continue
}
if line == "VkPhysicalDeviceMemoryProperties:" {
inMemoryProperties = true
inMemoryHeaps = false
flushHeap()
continue
}
if !inMemoryProperties {
continue
}
if strings.HasPrefix(line, "memoryHeaps:") {
inMemoryHeaps = true
continue
}
if strings.HasPrefix(line, "memoryTypes:") {
flushHeap()
inMemoryProperties = false
inMemoryHeaps = false
continue
}
if !inMemoryHeaps {
continue
}
if strings.HasPrefix(line, "memoryHeaps[") {
flushHeap()
inHeap = true
continue
}
if !inHeap {
continue
}
if strings.HasPrefix(line, "size") {
if size, ok := parseVulkanUintValue(line); ok {
heapSize = size
}
continue
}
if strings.Contains(line, "MEMORY_HEAP_DEVICE_LOCAL_BIT") {
heapDeviceLocal = true
}
}
flushHeap()
flushGPU()
return gpus
}
func parseVulkanGPUHeader(line string) (int, bool) {
if !strings.HasPrefix(line, "GPU") || !strings.HasSuffix(line, ":") {
return 0, false
}
index, err := strconv.Atoi(strings.TrimSuffix(strings.TrimPrefix(line, "GPU"), ":"))
if err != nil {
return 0, false
}
return index, true
}
func parseVulkanValue(line string) string {
_, value, ok := strings.Cut(line, "=")
if !ok {
return ""
}
return strings.TrimSpace(value)
}
func parseVulkanUintValue(line string) (uint64, bool) {
value := parseVulkanValue(line)
fields := strings.Fields(value)
if len(fields) == 0 {
return 0, false
}
parsed, err := strconv.ParseUint(fields[0], 0, 64)
if err != nil {
return 0, false
}
return parsed, true
}
// getAppleGPUMemory detects Apple Silicon GPUs using system_profiler (macOS only).
// Apple Silicon uses unified memory, so GPU memory is reported as system RAM.
func getAppleGPUMemory() []GPUMemoryInfo {