use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::ptr;
use std::sync::{Mutex, PoisonError};
use ruda_core::ir::AdapterLuid;
use windows_sys::Win32::System::Performance::{
PDH_CSTATUS_NEW_DATA, PDH_CSTATUS_VALID_DATA, PDH_FMT_COUNTERVALUE_ITEM_W, PDH_FMT_DOUBLE,
PDH_HCOUNTER, PDH_HQUERY, PDH_MORE_DATA, PDH_NO_DATA, PdhAddEnglishCounterW, PdhCloseQuery,
PdhCollectQueryData, PdhGetFormattedCounterArrayW, PdhOpenQueryW,
};
use super::gpu_engine_instance::{GpuEngine, GpuEngineInstance};
use crate::runtime::monitoring::{DeviceUtilization, UtilizationUnavailable};
const GPU_ENGINE_UTILIZATION_COUNTER_PATH: &str = "\\GPU Engine(*)\\Utilization Percentage";
#[derive(Debug)]
pub struct GpuEngineCounters {
adapter: AdapterLuid,
query: Mutex<GpuEngineQuery>,
}
#[derive(Debug)]
struct GpuEngineQuery {
query: PDH_HQUERY,
counter: PDH_HCOUNTER,
has_collected_once: bool,
item_buffer: Vec<u64>,
instance_name: String,
busy_percent_by_engine: BTreeMap<GpuEngine, f64>,
}
unsafe impl Send for GpuEngineQuery {}
impl GpuEngineCounters {
pub fn new(adapter: AdapterLuid) -> Result<Self, UtilizationUnavailable> {
let mut query = GpuEngineQuery {
query: ptr::null_mut(),
counter: ptr::null_mut(),
has_collected_once: false,
item_buffer: Vec::new(),
instance_name: String::new(),
busy_percent_by_engine: BTreeMap::new(),
};
let status = unsafe { PdhOpenQueryW(ptr::null(), 0, &mut query.query) };
GpuEngineQuery::succeeded("PdhOpenQueryW", status)?;
let counter_path: Vec<u16> = GPU_ENGINE_UTILIZATION_COUNTER_PATH
.encode_utf16()
.chain([0])
.collect();
let status = unsafe {
PdhAddEnglishCounterW(query.query, counter_path.as_ptr(), 0, &mut query.counter)
};
GpuEngineQuery::succeeded("PdhAddEnglishCounterW", status)?;
Ok(Self {
adapter,
query: Mutex::new(query),
})
}
pub fn read(&self) -> Result<DeviceUtilization, UtilizationUnavailable> {
self.query
.lock()
.unwrap_or_else(PoisonError::into_inner)
.busiest_engine_of(self.adapter)
}
}
impl GpuEngineQuery {
fn busiest_engine_of(
&mut self,
adapter: AdapterLuid,
) -> Result<DeviceUtilization, UtilizationUnavailable> {
let status = unsafe { PdhCollectQueryData(self.query) };
Self::succeeded("PdhCollectQueryData", status)?;
if !core::mem::replace(&mut self.has_collected_once, true) {
return Err(UtilizationUnavailable::NoPreviousReading);
}
let item_count = self.collect_formatted_items()?;
self.sum_busy_percent_by_engine_of(adapter, item_count);
let busiest_engine = self
.busy_percent_by_engine
.values()
.copied()
.fold(0.0, f64::max);
Ok(DeviceUtilization::new(busiest_engine as f32))
}
fn collect_formatted_items(&mut self) -> Result<usize, UtilizationUnavailable> {
loop {
let mut buffer_bytes = (self.item_buffer.len() * size_of::<u64>()) as u32;
let mut item_count = 0;
let status = unsafe {
PdhGetFormattedCounterArrayW(
self.counter,
PDH_FMT_DOUBLE,
&mut buffer_bytes,
&mut item_count,
self.item_buffer.as_mut_ptr().cast(),
)
};
match status {
0 => return Ok(item_count as usize),
PDH_NO_DATA => return Ok(0),
PDH_MORE_DATA => self
.item_buffer
.resize((buffer_bytes as usize).div_ceil(size_of::<u64>()), 0),
status => {
return Err(UtilizationUnavailable::QueryFailed(format!(
"PdhGetFormattedCounterArrayW returned {status:#010x}"
)));
}
}
}
}
fn sum_busy_percent_by_engine_of(&mut self, adapter: AdapterLuid, item_count: usize) {
self.busy_percent_by_engine.clear();
let items = unsafe {
core::slice::from_raw_parts(
self.item_buffer
.as_ptr()
.cast::<PDH_FMT_COUNTERVALUE_ITEM_W>(),
item_count,
)
};
for item in items {
let status = item.FmtValue.CStatus;
if status != PDH_CSTATUS_VALID_DATA && status != PDH_CSTATUS_NEW_DATA {
continue;
}
Self::decode_instance_name(item.szName, &mut self.instance_name);
let Some(instance) = GpuEngineInstance::parse(&self.instance_name) else {
continue;
};
if instance.adapter != adapter {
continue;
}
let busy_percent = unsafe { item.FmtValue.Anonymous.doubleValue };
*self
.busy_percent_by_engine
.entry(instance.engine)
.or_default() += busy_percent;
}
}
fn decode_instance_name(wide_name: *const u16, into: &mut String) {
into.clear();
if wide_name.is_null() {
return;
}
let length = (0..)
.take_while(|&index| unsafe { *wide_name.add(index) } != 0)
.count();
let units = unsafe { core::slice::from_raw_parts(wide_name, length) };
into.extend(
char::decode_utf16(units.iter().copied())
.map(|unit| unit.unwrap_or(char::REPLACEMENT_CHARACTER)),
);
}
fn succeeded(function: &str, status: u32) -> Result<(), UtilizationUnavailable> {
match status {
0 => Ok(()),
status => Err(UtilizationUnavailable::QueryFailed(format!(
"{function} returned {status:#010x}"
))),
}
}
}
impl Drop for GpuEngineQuery {
fn drop(&mut self) {
if !self.query.is_null() {
unsafe { PdhCloseQuery(self.query) };
}
}
}