use serde_json::Value;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use std::process::Command;
use std::str;
use std::sync::OnceLock;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
#[derive(Clone, Copy)]
enum AmdGpuCommand {
AmdSmi,
RocmSmi,
}
static AMDGPU_COMMAND: OnceLock<Option<AmdGpuCommand>> = OnceLock::new();
fn amdgpu_smi_output(command_name: &str, args: &[&str]) -> std::io::Result<std::process::Output> {
let mut cmd = Command::new(command_name);
cmd.args(args);
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.output()
}
pub fn get_amdgpu_power() -> f64 {
let Some(command) = detect_amdgpu_command() else {
return 0.0;
};
let (command_name, args): (&str, &[&str]) = match command {
AmdGpuCommand::AmdSmi => ("amd-smi", &["metric", "-p", "--json"]),
AmdGpuCommand::RocmSmi => ("rocm-smi", &["--showpower", "--json"]),
};
match amdgpu_smi_output(command_name, args) {
Ok(output) => {
if !output.status.success() {
crate::logging::print_warning(&format!(
"AMD GPU command {} exited with non-zero status",
command_name
));
return 0.0;
}
if let Ok(json_str) = str::from_utf8(&output.stdout) {
parse_amdgpu_json(json_str)
} else {
crate::logging::print_warning("Failed to read AMD GPU output as UTF-8");
0.0
}
}
Err(e) => {
crate::logging::print_warning(&format!("Failed to execute AMD GPU command: {}", e));
0.0
}
}
}
pub fn is_amdgpu_supported() -> bool {
detect_amdgpu_command().is_some()
}
fn detect_amdgpu_command() -> Option<AmdGpuCommand> {
*AMDGPU_COMMAND.get_or_init(|| {
if is_command_available("amd-smi") {
Some(AmdGpuCommand::AmdSmi)
} else if is_command_available("rocm-smi") {
Some(AmdGpuCommand::RocmSmi)
} else {
None
}
})
}
fn is_command_available(command: &str) -> bool {
let mut cmd = Command::new(command);
cmd.arg("--version");
#[cfg(target_os = "windows")]
{
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.output().is_ok()
}
fn parse_amdgpu_json(json_str: &str) -> f64 {
let v: Value = match serde_json::from_str(json_str) {
Ok(v) => v,
Err(_) => return 0.0,
};
match v {
Value::Object(_) | Value::Array(_) => extract_total_power(&v),
_ => 0.0,
}
}
fn extract_total_power(value: &Value) -> f64 {
match value {
Value::Object(obj) => {
if let Some(p) = extract_power_from_object(obj) {
p
} else {
obj.values().map(extract_total_power).sum()
}
}
Value::Array(arr) => arr.iter().map(extract_total_power).sum(),
_ => 0.0,
}
}
fn extract_power_from_object(obj: &serde_json::Map<String, Value>) -> Option<f64> {
const POWER_KEYS: &[&str] = &[
"Average Graphics Package Power (W)",
"Current Socket Graphics Package Power (W)",
"current_socket_graphics_package_power",
"current_graphics_package_power",
"average_socket_power",
"socket_power",
"current_socket_power",
"Power (W)",
"Power",
"average_power",
];
for &key in POWER_KEYS {
if let Some(power_val) = obj.get(key)
&& let Some(p) = parse_power_value(power_val)
{
return Some(p);
}
}
if let Some(power_obj) = obj.get("power").and_then(Value::as_object) {
return extract_power_from_object(power_obj);
}
None
}
fn parse_power_value(value: &Value) -> Option<f64> {
if let Some(p) = value.as_f64() {
return Some(p);
}
if let Some(s) = value.as_str() {
return parse_numeric_string(s);
}
if let Some(obj) = value.as_object()
&& let Some(v) = obj.get("value")
{
return parse_power_value(v);
}
None
}
fn parse_numeric_string(s: &str) -> Option<f64> {
s.trim().parse::<f64>().ok().or_else(|| {
let first_token = s.split_whitespace().next()?;
first_token.parse::<f64>().ok()
})
}