articles

sysinfo: Getting GPUs

A while ago (5 years T_T), I wrote two blog posts (here and here) explaining how the Rust sysinfo crate retrieves information from systems. I recently added support to retrieve GPU information on Linux, macOS and Windows, and I think it's interesting to explain how it's done, so here we are (and also it took me 3 months to write this new feature so I'm definitely going to talk about it :3).

Without further ado, let's start with the (surprisingly) simplest system to retrieve this information: macOS.

macOS

Yes, you read that right: macOS was the system which took me the less time to query GPU information. Let's dive into it.

macOS provides an API which is very nice to use if you code in Objective-C or Swift, quite awful for everything else.

First, we get get the handle for the IOAccelerator service (with IOServiceMatching()), then we retrieve the matching services (with IOServiceGetMatchingServices()) which returns an iterator. Each item in this iterator is an IO device, but not all of them are a GPU. So first, we need to filter only the ones we're interested in.

To do so, we need to get the parent entry of this "IO device" (with IORegistryEntryGetParentEntry()) and check if:

  1. It's an "IO-PCI" device.
  2. If its PCI class-code is a GPU's (the code being 0x03, and yes I could have written 3, but hex looks more fancy).

Important thing to note: PCI information like the class-code is the same on Windows and Linux. "PCI" means "Peripheral Component Interconnect". It's an architecture used to identify and manage hardware devices. I recommend checking out the PCI wikipedia page if you want more information.

For our case, we just need to know that a PCI has a domain, a bus, a device and a function.

So once we have confirmed it's a GPU, it's time to retrieve its information. Since we already have the correct device, we just need to ask for its related information with the right key:

One last thing we retrieve is the vendor-id. I mentioned above that PCI is common between Windows, Linux and macOS (and likely all platforms but didn't check). On linux, you can retrieve all the PCI information in /usr/share/hwdata/pci.ids. Since it's the same for all platforms (as far as I know), I generated a match where the vendor ID returns a string. This code is common to Windows, Linux and macOS in sysinfo (for Linux, it's only as a backup in case the PCI file is not present on the system).

Now what about the GPU memory? As far as I know, on macOS, the GPU memory is always integrated with the CPU's, meaning they share the same RAM. sysinfo API only provides VRAM, not the shared memory, so nothing else to be done for macOS.

Linux

Unlike other platforms (for GPU), there isn't a common API to query GPU information beyond a very limited set. So first, we read entries from the /sys/class/drm folder and only keep folders looking like card[0-9]+.

Inside of each of these card, we get the PCI by reading the device link (so /sys/class/drm/card0/device pointing to something like ../../../0000:00:00.0/). The filename (0000:00:00.0) is the PCI.

To confirm it's a GPU, we read device/class. Remember the 0x03 check we did on macOS? Exactly the same here: if the content of this file starts with 0x03 (written as is), then it's a GPU.

Next information we retrieve here is the vendor ID, from the device/vendor file which only contains an hex number. For Linux, this is likely THE most important information as everything else we can query will come from knowing the vendor ID.

So, I mentioned the /usr/share/hwdata/pci.ids file in the macOS section. Well, it's time to put it into use: this file contains the mapping between the vendor ID and the vendor name, but also between the model ID and the model name. And if you want to know what kind of device you're checking in device/class, the list is also in this file. The 0x03 we use to know if it's a GPU is also available in this file.

So based on the vendor ID, we have 3 different paths ahead of us, let's start with the easiest: AMD.

AMD

AMD is magical, AMD is good, AMD doesn't require a library to get GPU information, it provides files!

You want the GPU % usage? Read device/gpu_busy_percent.

You want the GPU total VRAM? Read mem_info_vram_total.

You want the GPU used VRAM? Read mem_info_vram_used.

And that's it. We're done. It's simple, it works, it's magical. I'm not used to that when trying to retrieve system information...

NVIDIA

For NVIDIA, you use a library named nvml. And because I don't want to require this library to present at compile-time so sysinfo can work without requiring users to have it installed, it means I have to load it dynamically. Fun times ahead!

So, I do that (with dlsym), I also load the symbols of the functions I will need and I redeclared types and constants I needed. Once done, we initialize the library with nvmlInit_v2 (don't forget to clean with nvmlShutdown). We're now ready to go.

First thing to do: get the count of NVIDIA GPUs with nvmlDeviceGetCount_v2. Then we iterate (0..count) and get the handle for each GPU using nvmlDeviceGetHandleByIndex_v2. To make it simpler, here's the list of information with the function I used to query it:

Overall, if we forget that we need to load dynamically its library, it's mostly ok. The API is easy enough to navigate.

Other GPUs

Because AMD and NVIDIA aren't the only GPU providers, we also need to handle these ones. And because I'm too lazy to look if they provide a library, I default to using vulkan. This choice also means that older GPUs that don't support vulkan will simply be ignored. But considering vulkan has been around for more than a decade, I think it's fine.

So again, I don't want to force users to have vulkan installed to make sysinfo compile, we need to load the library dynamically, redefine all its types and constants, etc. You got the gist. Then we initialize the library with vkCreateInstance (don't forget to clean with vkDestroyInstance).

First, we enumerate the "physical devices" with vkEnumeratePhysicalDevices. Then for each information:

vulkan doesn't provide an API (as far as I know) to query the GPU % usage, so we cannot retrieve this information for other GPUs.

For both used and total VRAM, we need to loop through a field named memory_heaps and look for memory that is actually VRAM and not shared memory. As said previously, it's possible for a GPU to not have its own memory but instead use the system's memory (in particular for embedded GPUs).

Windows

By far the one I spent the most time on. Windows often has a lot of APIs to query the same information, but in subtly different ways which all come with their own limitations.

So first, we want to list all GPUs. To do so, we create a factory (?) with CreateDXGIFactory1 ("DXGI" means "Microsoft DirectX Graphics Infrastructure", and "DirectX" is the equivalent of "OpenGL" on Windows) and then we list adapters (??) with EnumAdapters1.

For each adapter, we get a descriptor with GetDesc1. And now comes the tricky part, we cannot directly get the PCI of an adapter, we need to use two other APIs for that.

We start with retrieving all PCIs. To do so, we call:

RunSetupDiGetClassDevsW(Some(&GUID_DEVCLASS_DISPLAY), None, None, DIGCF_PRESENT)

(Don't forget to clean with SetupDiDestroyDeviceInfoList)

I wrote the full function call because it took me a long time before I could figure out what the correct arguments for my use case were. So here we are. This returns a handle if everything went well.

With this handle, we now iterate all PCIs with SetupDiEnumDeviceInfo. For each PCI, to extract the information we want, we call SetupDiGetDeviceRegistryPropertyW to get a u32. First with SPDRP_BUSNUMBER to get the bus, and then with SPDRP_ADDRESS to get the address which contains the device and the function (the device is the "high" 16 bits and the function is the "low" 16 bits). As far as I know, there is no PCI domain on Windows, so that's all the information we have.

So now we have all the PCIs, but we still have a missing link between them and the adapters. Adapters have a "luid" field which we can use to retrieve its PCI. Yes, I previously wrote that we couldn't directly get the PCI of an adapter, and I stand by my words: it might be a "fake" adapter, not an actual GPU. The only way to know is to check if this adapter's PCI is present in the list of PCIs we retrieved.

So to get this PCI, we use D3DKMTOpenAdapterFromLuid (don't forget to clean with D3DKMTCloseAdapter) which returns a handle (yet another). This will allow us to map the LUID to an adapter handle. With this handle, we call D3DKMTQueryAdapterInfo which returns the PCI.

Now that we have the adapter's PCI and the list of all PCIs, we check if our adapter's PCI is in the list. If not, then it's not a real GPU (could be just an adapter, hence the name). In such a case, we simply skip it.

Back to our descriptor, we use its VendorId field and our vendor ID to vendor name map to get the vendor name. The model is in the Description field, the total VRAM in the DedicatedVideoMemory field.

To get the used VRAM, we need to cast our adapter into IDXGIAdapter3 and then call its QueryVideoMemoryInfo method. Then it's the CurrentUsage field.

And finally the last part: retrieving the GPU % usage. Windows provides a "query system/engine" which can be used to retrieve a lot of different information, so that's what we're going to use here.

We call PdhOpenQueryW to open a query then we add a new counter to it with PdhAddEnglishCounterW (and the r"\GPU Engine(*)\Utilization Percentage" key). That, you do it once at the beginning and never touch it again before the end of the program.

Then every time we want to get the GPU % usage, we call PdhCollectQueryData to ask the query system/engine to collect the data we need. Then we call PdhGetFormattedCounterArrayW to actually get the data (an array).

Now the interesting part: how do we know to which GPU each data refers to? Luckily, each array element contains the data and a LUID (in string format though, we need to parse it). The name looks like luid_0x15_0x45. We strip the luid_ part, we split on the _ and we convert each 0x number into a u32. The first number is the "high" part of the LUID and the other is the "low" part. Based on this, we simply need to iterate our list of GPUs, check which one has this LUID and update its GPU % usage.

Words of the end

Now you know how to query GPU information on Linux, Windows and macOS. It's quite a ride, and although unsurprised, still sad to see no unified API (in particular Linux).

My cat, fully accelerated.


my cat lying on his back on a bed
Posted on the 20/06/2026 at 01:15 by @GuillaumeGomez

Previous article

Rust GCC backend: Why and how
Back to articles list
RSS feedRSS feed