use std::collections::HashMap;
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::System::Performance::{
PDH_FMT_COUNTERVALUE, PDH_FMT_LARGE, PDH_HCOUNTER, PDH_HQUERY, PERF_DETAIL_WIZARD,
PdhAddCounterW, PdhCloseQuery, PdhCollectQueryData, PdhEnumObjectItemsW,
PdhGetFormattedCounterValue, PdhOpenQueryW,
};
use windows::Win32::System::Threading::{
OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW,
};
use windows::core::{PCWSTR, PWSTR, w};
use crate::{HypomnesisError, Result};
const PDH_SUCCESS: u32 = 0;
const PDH_MORE_DATA: u32 = 0x8000_07D2;
const PDH_CSTATUS_NO_OBJECT: u32 = 0xC000_0BB8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SegmentRow {
pid: u32,
segment_idx: u32,
used_bytes: u64,
}
struct QueryGuard {
handle: PDH_HQUERY,
}
impl Drop for QueryGuard {
fn drop(&mut self) {
#[allow(unsafe_code)]
unsafe {
let _ = PdhCloseQuery(self.handle);
}
}
}
#[must_use]
fn parse_instance_name(name: &str) -> Option<(u32, (i32, u32), u32)> {
let rest = name.strip_prefix("pid_")?;
let (pid_str, rest) = rest.split_once('_')?;
let pid: u32 = pid_str.parse().ok()?;
let rest = rest.strip_prefix("luid_0x")?;
let (high_str, rest) = rest.split_once("_0x")?;
let high_u32: u32 = u32::from_str_radix(high_str, 16).ok()?;
#[allow(clippy::as_conversions, clippy::cast_possible_wrap)]
let high = high_u32 as i32;
let (low_str, seg_str) = rest.split_once("_phys_")?;
let low: u32 = u32::from_str_radix(low_str, 16).ok()?;
let segment_idx: u32 = seg_str.parse().ok()?;
Some((pid, (high, low), segment_idx))
}
#[must_use]
fn parse_multi_string(buf: &[u16]) -> Vec<String> {
buf.split(|&c| c == 0)
.filter(|s| !s.is_empty())
.map(String::from_utf16_lossy)
.collect()
}
#[allow(unsafe_code)]
fn collect_segmented_rows(target_luid: (i32, u32)) -> Result<Vec<SegmentRow>> {
let mut raw_handle = PDH_HQUERY::default();
let status = unsafe { PdhOpenQueryW(PCWSTR::null(), 0, &raw mut raw_handle) };
if status != PDH_SUCCESS {
return Err(HypomnesisError::Pdh(format!(
"PdhOpenQueryW failed: 0x{status:08X}"
)));
}
let query = QueryGuard { handle: raw_handle };
let mut counter_size: u32 = 0;
let mut instance_size: u32 = 0;
let status = unsafe {
PdhEnumObjectItemsW(
PCWSTR::null(),
PCWSTR::null(),
w!("GPU Process Memory"),
None,
&raw mut counter_size,
None,
&raw mut instance_size,
PERF_DETAIL_WIZARD,
0,
)
};
if status == PDH_CSTATUS_NO_OBJECT {
return Err(HypomnesisError::Pdh(
"GPU Process Memory counter set not registered (pre-WDDM 2.0?)".to_owned(),
));
}
if status == PDH_SUCCESS && instance_size <= 1 {
return Ok(Vec::new());
}
if status != PDH_MORE_DATA {
return Err(HypomnesisError::Pdh(format!(
"PdhEnumObjectItemsW (size query) failed: 0x{status:08X}"
)));
}
#[allow(clippy::as_conversions)]
let mut counter_buffer: Vec<u16> = vec![0; counter_size as usize];
#[allow(clippy::as_conversions)]
let mut instance_buffer: Vec<u16> = vec![0; instance_size as usize];
let status = unsafe {
PdhEnumObjectItemsW(
PCWSTR::null(),
PCWSTR::null(),
w!("GPU Process Memory"),
Some(PWSTR::from_raw(counter_buffer.as_mut_ptr())),
&raw mut counter_size,
Some(PWSTR::from_raw(instance_buffer.as_mut_ptr())),
&raw mut instance_size,
PERF_DETAIL_WIZARD,
0,
)
};
if status != PDH_SUCCESS {
return Err(HypomnesisError::Pdh(format!(
"PdhEnumObjectItemsW (data fetch) failed: 0x{status:08X}"
)));
}
let instances = parse_multi_string(&instance_buffer);
#[cfg(feature = "debug-output")]
eprintln!("[PDH debug] enumerated {} instance(s)", instances.len());
let mut counter_handles: Vec<(u32, u32, PDH_HCOUNTER)> = Vec::new();
for instance in &instances {
let Some((pid, luid, segment_idx)) = parse_instance_name(instance) else {
continue;
};
if luid != target_luid {
continue;
}
let counter_path = format!("\\GPU Process Memory({instance})\\Dedicated Usage");
let counter_path_wide: Vec<u16> = counter_path
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let mut h_counter = PDH_HCOUNTER::default();
let status = unsafe {
PdhAddCounterW(
query.handle,
PCWSTR::from_raw(counter_path_wide.as_ptr()),
0,
&raw mut h_counter,
)
};
if status == PDH_SUCCESS {
counter_handles.push((pid, segment_idx, h_counter));
} else {
#[cfg(feature = "debug-output")]
eprintln!(
"[PDH debug] PdhAddCounterW failed for instance {instance:?}: 0x{status:08X} (skipped)"
);
}
}
if counter_handles.is_empty() {
return Ok(Vec::new());
}
let status = unsafe { PdhCollectQueryData(query.handle) };
if status != PDH_SUCCESS {
return Err(HypomnesisError::Pdh(format!(
"PdhCollectQueryData failed: 0x{status:08X}"
)));
}
let mut rows: Vec<SegmentRow> = Vec::with_capacity(counter_handles.len());
for (pid, segment_idx, h_counter) in counter_handles {
let mut value = PDH_FMT_COUNTERVALUE::default();
let status =
unsafe { PdhGetFormattedCounterValue(h_counter, PDH_FMT_LARGE, None, &raw mut value) };
if status != PDH_SUCCESS {
#[cfg(feature = "debug-output")]
eprintln!(
"[PDH debug] PdhGetFormattedCounterValue failed for pid={pid} seg={segment_idx}: 0x{status:08X} (skipped)"
);
continue;
}
let raw_value: i64 = unsafe { value.Anonymous.largeValue };
if raw_value < 0 {
#[cfg(feature = "debug-output")]
eprintln!(
"[PDH debug] negative Dedicated Usage for pid={pid} seg={segment_idx}: {raw_value} (skipped)"
);
continue;
}
#[allow(clippy::as_conversions, clippy::cast_sign_loss)]
let used_bytes = raw_value as u64;
rows.push(SegmentRow {
pid,
segment_idx,
used_bytes,
});
}
Ok(rows)
}
pub(super) fn query_per_process_vram(device_index: u32) -> Result<Vec<(u32, u64)>> {
let target_luid = super::dxgi::adapter_luid(device_index).ok_or_else(|| {
HypomnesisError::Pdh(format!(
"no NVIDIA adapter at device_index {device_index} via DXGI walk"
))
})?;
let segments = collect_segmented_rows(target_luid)?;
let mut by_pid: HashMap<u32, u64> = HashMap::with_capacity(segments.len());
for row in segments {
by_pid
.entry(row.pid)
.and_modify(|acc| *acc = acc.saturating_add(row.used_bytes))
.or_insert(row.used_bytes);
}
Ok(by_pid.into_iter().collect())
}
struct HandleGuard {
handle: HANDLE,
}
impl Drop for HandleGuard {
fn drop(&mut self) {
#[allow(unsafe_code)]
unsafe {
let _ = CloseHandle(self.handle);
}
}
}
const NAME_BUF_LEN: usize = 1024;
const KERNEL_PROCESS_NAME: &str = "[kernel]";
#[must_use]
const fn kernel_name_for_pid(pid: u32) -> Option<&'static str> {
if pid == 4 {
Some(KERNEL_PROCESS_NAME)
} else {
None
}
}
#[allow(unsafe_code)]
#[must_use]
pub(super) fn name_from_pid_windows(pid: u32) -> Option<String> {
if let Some(synthetic) = kernel_name_for_pid(pid) {
return Some(synthetic.to_owned());
}
let raw_handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }.ok()?;
let guard = HandleGuard { handle: raw_handle };
let mut buf = [0_u16; NAME_BUF_LEN];
#[allow(clippy::as_conversions, clippy::cast_possible_truncation)]
let mut size: u32 = NAME_BUF_LEN as u32;
let result = unsafe {
QueryFullProcessImageNameW(
guard.handle,
PROCESS_NAME_WIN32,
PWSTR::from_raw(buf.as_mut_ptr()),
&raw mut size,
)
};
if result.is_err() {
return None;
}
#[allow(clippy::as_conversions)]
let written = (size as usize).min(NAME_BUF_LEN);
#[allow(clippy::indexing_slicing)]
let full_path = String::from_utf16_lossy(&buf[..written]);
let base = basename_from_path(&full_path);
if base.is_empty() { None } else { Some(base) }
}
#[must_use]
fn basename_from_path(path: &str) -> String {
path.rsplit_once(['\\', '/'])
.map_or_else(|| path.to_owned(), |(_, base)| base.to_owned())
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::missing_docs_in_private_items
)]
mod tests {
use super::{basename_from_path, kernel_name_for_pid, parse_instance_name, parse_multi_string};
#[test]
fn parse_instance_name_basic() {
let (pid, luid, seg) =
parse_instance_name("pid_24168_luid_0x00000000_0x00012345_phys_0").unwrap();
assert_eq!(pid, 24168);
assert_eq!(luid, (0, 0x0001_2345));
assert_eq!(seg, 0);
}
#[test]
fn parse_instance_name_nonzero_segment() {
let (pid, _luid, seg) =
parse_instance_name("pid_4242_luid_0x00000000_0x00000abc_phys_3").unwrap();
assert_eq!(pid, 4242);
assert_eq!(seg, 3);
}
#[test]
fn parse_instance_name_high_bit_luid() {
let (_pid, luid, _seg) =
parse_instance_name("pid_1_luid_0xFFFFFFFF_0x00000001_phys_0").unwrap();
assert_eq!(luid.0, -1_i32);
assert_eq!(luid.1, 1);
}
#[test]
fn parse_instance_name_full_low_part() {
let (_pid, luid, _seg) =
parse_instance_name("pid_1_luid_0x00000000_0xDEADBEEF_phys_0").unwrap();
assert_eq!(luid, (0, 0xDEAD_BEEF));
}
#[test]
fn parse_instance_name_rejects_missing_prefix() {
assert!(parse_instance_name("luid_0x00000000_0x00000001_phys_0").is_none());
assert!(parse_instance_name("_Total").is_none());
assert!(parse_instance_name("").is_none());
}
#[test]
fn parse_instance_name_rejects_non_hex_luid() {
assert!(parse_instance_name("pid_1_luid_0xZZZZZZZZ_0x00000001_phys_0").is_none());
assert!(parse_instance_name("pid_1_luid_0x00000000_0xZZZZZZZZ_phys_0").is_none());
}
#[test]
fn parse_instance_name_rejects_non_numeric_pid() {
assert!(parse_instance_name("pid_abc_luid_0x00000000_0x00000001_phys_0").is_none());
}
#[test]
fn parse_instance_name_rejects_missing_phys_suffix() {
assert!(parse_instance_name("pid_1_luid_0x00000000_0x00000001").is_none());
assert!(parse_instance_name("pid_1_luid_0x00000000_0x00000001_phys_").is_none());
}
#[test]
fn parse_multi_string_basic() {
let buf: Vec<u16> = "foo\0bar\0\0".encode_utf16().collect();
let parsed = parse_multi_string(&buf);
assert_eq!(parsed, vec!["foo".to_owned(), "bar".to_owned()]);
}
#[test]
fn parse_multi_string_empty() {
let buf: Vec<u16> = vec![0, 0];
let parsed = parse_multi_string(&buf);
assert!(parsed.is_empty());
}
#[test]
fn parse_multi_string_single_entry() {
let buf: Vec<u16> = "only\0\0".encode_utf16().collect();
let parsed = parse_multi_string(&buf);
assert_eq!(parsed, vec!["only".to_owned()]);
}
#[test]
fn basename_from_path_windows_backslashes() {
assert_eq!(
basename_from_path("C:\\Program Files\\Mozilla Firefox\\firefox.exe"),
"firefox.exe"
);
}
#[test]
fn basename_from_path_forward_slashes() {
assert_eq!(
basename_from_path("C:/Users/Eric/AppData/ollama.exe"),
"ollama.exe"
);
}
#[test]
fn basename_from_path_mixed_separators() {
assert_eq!(
basename_from_path("C:\\Users/Eric\\AppData/ollama.exe"),
"ollama.exe"
);
}
#[test]
fn basename_from_path_no_separator() {
assert_eq!(basename_from_path("System"), "System");
assert_eq!(basename_from_path("idle.exe"), "idle.exe");
}
#[test]
fn basename_from_path_empty_input() {
assert_eq!(basename_from_path(""), "");
}
#[test]
fn basename_from_path_trailing_separator() {
assert_eq!(basename_from_path("C:\\Windows\\"), "");
assert_eq!(basename_from_path("/tmp/"), "");
}
#[test]
fn basename_from_path_single_separator() {
assert_eq!(basename_from_path("\\"), "");
assert_eq!(basename_from_path("/"), "");
}
#[test]
fn kernel_name_for_pid_recognises_pid_4() {
assert_eq!(kernel_name_for_pid(4), Some("[kernel]"));
}
#[test]
fn kernel_name_for_pid_returns_none_for_other_pids() {
assert_eq!(kernel_name_for_pid(0), None);
assert_eq!(kernel_name_for_pid(1), None);
assert_eq!(kernel_name_for_pid(3), None);
assert_eq!(kernel_name_for_pid(5), None);
assert_eq!(kernel_name_for_pid(1000), None);
assert_eq!(kernel_name_for_pid(u32::MAX), None);
}
}