|
1 | 1 | use clap::{Args, CommandFactory, Parser, Subcommand}; |
2 | 2 | use clap_complete::{generate, Generator, Shell}; |
3 | 3 | use nvml_wrapper::{error::NvmlError, Device, Nvml}; |
| 4 | +use nvml_wrapper_sys::bindings::{ |
| 5 | + nvmlDevice_t, nvmlReturn_enum_NVML_SUCCESS, |
| 6 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_CURR, |
| 7 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_MAX, |
| 8 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_MIN, NvmlLib, |
| 9 | +}; |
4 | 10 | use serde::Deserialize; |
5 | 11 | use std::{collections::HashMap, io}; |
6 | 12 |
|
@@ -64,6 +70,11 @@ struct Sets { |
64 | 70 | /// GPU max memory clock |
65 | 71 | #[arg(long, requires = "min_mem_clock")] |
66 | 72 | max_mem_clock: Option<u32>, |
| 73 | + /// Target temperature in Celsius (acoustic limit). The GPU will automatically |
| 74 | + /// adjust fan curves to maintain this temperature. Similar to MSI Afterburner's |
| 75 | + /// "target temperature" feature. |
| 76 | + #[arg(short, long)] |
| 77 | + target_temp: Option<u32>, |
67 | 78 | } |
68 | 79 |
|
69 | 80 | impl Sets { |
@@ -121,6 +132,20 @@ impl Sets { |
121 | 132 | .set_mem_locked_clocks(min_mem_clock, max_mem_clock) |
122 | 133 | .expect("Failed to set GPU min and max memory clocks"); |
123 | 134 | } |
| 135 | + |
| 136 | + if let Some(target_temp) = self.target_temp { |
| 137 | + if let Err(e) = set_acoustic_temperature(device, target_temp) { |
| 138 | + let (min, max) = get_acoustic_temperature_range(device); |
| 139 | + let mut error_msg = format!( |
| 140 | + "Failed to set target temperature: {}°C - {}", |
| 141 | + target_temp, e |
| 142 | + ); |
| 143 | + if let (Some(min), Some(max)) = (min, max) { |
| 144 | + error_msg.push_str(&format!(" Valid range: {}°C - {}°C", min, max)); |
| 145 | + } |
| 146 | + panic!("{}", error_msg); |
| 147 | + } |
| 148 | + } |
124 | 149 | } |
125 | 150 | } |
126 | 151 |
|
@@ -179,6 +204,20 @@ fn main() { |
179 | 204 | ), |
180 | 205 | Err(e) => eprintln!("Failed to get GPU power limit constraints: {:?}", e), |
181 | 206 | } |
| 207 | + |
| 208 | + // Target temperature (acoustic limit) |
| 209 | + match get_acoustic_temperature(&device) { |
| 210 | + Some(temp) => println!("Target temperature (acoustic): {}°C", temp), |
| 211 | + None => eprintln!("Failed to get target temperature (not supported or not set)"), |
| 212 | + } |
| 213 | + |
| 214 | + let (min_temp, max_temp) = get_acoustic_temperature_range(&device); |
| 215 | + match (min_temp, max_temp) { |
| 216 | + (Some(min), Some(max)) => { |
| 217 | + println!("Target temperature range: {}°C - {}°C", min, max) |
| 218 | + } |
| 219 | + _ => eprintln!("Failed to get target temperature range (not supported)"), |
| 220 | + } |
182 | 221 | } |
183 | 222 | None => { |
184 | 223 | let Ok(config_file) = std::fs::read_to_string(cli.file) else { |
@@ -227,3 +266,109 @@ fn generate_completion_script<G: Generator>(gen: G) { |
227 | 266 | let name = cmd.get_name().to_string(); |
228 | 267 | generate(gen, &mut cmd, name, &mut io::stdout()); |
229 | 268 | } |
| 269 | + |
| 270 | +/// Gets the raw NVML device handle from a Device. |
| 271 | +/// This is needed to call low-level NVML functions not exposed by nvml-wrapper. |
| 272 | +fn get_raw_device_handle(device: &Device) -> nvmlDevice_t { |
| 273 | + // SAFETY: Device stores the raw handle as the first field in its struct. |
| 274 | + // We access it by transmuting the reference. |
| 275 | + unsafe { std::ptr::read(device as *const Device as *const nvmlDevice_t) } |
| 276 | +} |
| 277 | + |
| 278 | +/// Sets the acoustic (target) temperature threshold. |
| 279 | +/// The GPU will automatically adjust fan curves to maintain this temperature. |
| 280 | +fn set_acoustic_temperature(device: &Device, temp_celsius: u32) -> Result<(), String> { |
| 281 | + let handle = get_raw_device_handle(device); |
| 282 | + let mut temp = temp_celsius as i32; |
| 283 | + |
| 284 | + // Load the NVML library |
| 285 | + let nvml_lib = unsafe { |
| 286 | + NvmlLib::new("libnvidia-ml.so.1") |
| 287 | + .or_else(|_| NvmlLib::new("libnvidia-ml.so")) |
| 288 | + .map_err(|e| format!("Failed to load NVML library: {:?}", e))? |
| 289 | + }; |
| 290 | + |
| 291 | + let result = unsafe { |
| 292 | + nvml_lib.nvmlDeviceSetTemperatureThreshold( |
| 293 | + handle, |
| 294 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_CURR, |
| 295 | + &mut temp, |
| 296 | + ) |
| 297 | + }; |
| 298 | + |
| 299 | + if result == nvmlReturn_enum_NVML_SUCCESS { |
| 300 | + Ok(()) |
| 301 | + } else { |
| 302 | + Err(format!("NVML error code: {}", result)) |
| 303 | + } |
| 304 | +} |
| 305 | + |
| 306 | +/// Gets the current acoustic (target) temperature threshold. |
| 307 | +fn get_acoustic_temperature(device: &Device) -> Option<u32> { |
| 308 | + let handle = get_raw_device_handle(device); |
| 309 | + let mut temp: u32 = 0; |
| 310 | + |
| 311 | + let nvml_lib = unsafe { |
| 312 | + NvmlLib::new("libnvidia-ml.so.1") |
| 313 | + .or_else(|_| NvmlLib::new("libnvidia-ml.so")) |
| 314 | + .ok()? |
| 315 | + }; |
| 316 | + |
| 317 | + let result = unsafe { |
| 318 | + nvml_lib.nvmlDeviceGetTemperatureThreshold( |
| 319 | + handle, |
| 320 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_CURR, |
| 321 | + &mut temp, |
| 322 | + ) |
| 323 | + }; |
| 324 | + |
| 325 | + if result == nvmlReturn_enum_NVML_SUCCESS { |
| 326 | + Some(temp) |
| 327 | + } else { |
| 328 | + None |
| 329 | + } |
| 330 | +} |
| 331 | + |
| 332 | +/// Gets the min and max acoustic temperature range. |
| 333 | +fn get_acoustic_temperature_range(device: &Device) -> (Option<u32>, Option<u32>) { |
| 334 | + let handle = get_raw_device_handle(device); |
| 335 | + let mut min_temp: u32 = 0; |
| 336 | + let mut max_temp: u32 = 0; |
| 337 | + |
| 338 | + let nvml_lib = match unsafe { |
| 339 | + NvmlLib::new("libnvidia-ml.so.1").or_else(|_| NvmlLib::new("libnvidia-ml.so")) |
| 340 | + } { |
| 341 | + Ok(lib) => lib, |
| 342 | + Err(_) => return (None, None), |
| 343 | + }; |
| 344 | + |
| 345 | + let min_result = unsafe { |
| 346 | + nvml_lib.nvmlDeviceGetTemperatureThreshold( |
| 347 | + handle, |
| 348 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_MIN, |
| 349 | + &mut min_temp, |
| 350 | + ) |
| 351 | + }; |
| 352 | + |
| 353 | + let max_result = unsafe { |
| 354 | + nvml_lib.nvmlDeviceGetTemperatureThreshold( |
| 355 | + handle, |
| 356 | + nvmlTemperatureThresholds_enum_NVML_TEMPERATURE_THRESHOLD_ACOUSTIC_MAX, |
| 357 | + &mut max_temp, |
| 358 | + ) |
| 359 | + }; |
| 360 | + |
| 361 | + let min = if min_result == nvmlReturn_enum_NVML_SUCCESS { |
| 362 | + Some(min_temp) |
| 363 | + } else { |
| 364 | + None |
| 365 | + }; |
| 366 | + |
| 367 | + let max = if max_result == nvmlReturn_enum_NVML_SUCCESS { |
| 368 | + Some(max_temp) |
| 369 | + } else { |
| 370 | + None |
| 371 | + }; |
| 372 | + |
| 373 | + (min, max) |
| 374 | +} |
0 commit comments