2025-04-14 21:19:50 +03:00
use super ::{ CommonControllerInfo , FanControlHandle , GpuController , VENDOR_AMD };
2025-04-23 22:11:56 +03:00
use crate ::server ::{
gpu_controller ::fan_control ::FanCurveExt , opencl ::get_opencl_info , vulkan ::get_vulkan_info ,
};
2024-10-25 23:35:26 +03:00
use amdgpu_sysfs ::{
error ::Error ,
gpu_handle ::{
fan_control ::FanCurve as PmfwCurve ,
overdrive ::{ ClocksTable , ClocksTableGen },
power_profile_mode ::PowerProfileModesTable ,
CommitHandle , GpuHandle , PerformanceLevel , PowerLevelKind , PowerLevels ,
},
hw_mon ::{ FanControlMethod , HwMon },
2025-03-16 18:40:03 +02:00
sysfs ::SysFS ,
2024-10-25 23:35:26 +03:00
};
use anyhow ::{ anyhow , Context };
2025-03-15 13:37:35 +02:00
use futures ::{ future ::LocalBoxFuture , FutureExt };
2024-10-25 23:35:26 +03:00
use lact_schema ::{
2025-04-14 21:19:50 +03:00
config ::{ ClocksConfiguration , FanControlSettings , FanCurve , GpuConfig },
2025-01-12 17:16:02 +02:00
ClocksInfo , ClockspeedStats , DeviceInfo , DeviceStats , DrmInfo , FanStats , IntelDrmInfo ,
2025-05-05 18:15:59 +03:00
LinkInfo , PmfwInfo , PowerState , PowerStates , PowerStats , RopInfo , VoltageStats , VramStats ,
2024-10-25 23:35:26 +03:00
};
2025-05-16 17:35:58 +03:00
use libdrm_amdgpu_sys ::AMDGPU ::{ GpuMetrics , ThrottlerBit };
2025-05-26 06:22:19 +09:00
use libdrm_amdgpu_sys ::{ LibDrmAmdgpu , AMDGPU ::SENSOR_INFO ::SENSOR_TYPE , PCI };
2024-10-25 23:35:26 +03:00
use std ::{
cell ::RefCell ,
cmp ,
2025-03-30 15:39:10 +03:00
collections ::{ HashMap , HashSet , VecDeque },
2025-01-12 17:16:02 +02:00
path ::PathBuf ,
2024-10-25 23:35:26 +03:00
rc ::Rc ,
time ::Duration ,
};
use std ::{ collections ::BTreeMap , fs , time ::Instant };
2025-03-30 15:39:10 +03:00
use tokio ::{ select , sync ::Notify , time ::sleep };
2024-10-25 23:35:26 +03:00
use tracing ::{ debug , error , info , trace , warn };
use {
lact_schema ::DrmMemoryInfo ,
libdrm_amdgpu_sys ::AMDGPU ::{ DeviceHandle as DrmHandle , MetricsInfo , GPU_INFO },
};
2025-03-01 09:56:10 +02:00
const FAN_CONTROL_RETRIES : u32 = 10 ;
2024-10-25 23:35:26 +03:00
const MAX_PSTATE_READ_ATTEMPTS : u32 = 5 ;
2024-11-10 21:13:19 +02:00
const STEAM_DECK_IDS : [ & str ; 2 ] = [ "163F" , "1435" ];
2024-10-25 23:35:26 +03:00
pub struct AmdGpuController {
handle : GpuHandle ,
drm_handle : Option < DrmHandle > ,
2025-01-12 17:16:02 +02:00
common : CommonControllerInfo ,
2024-10-25 23:35:26 +03:00
fan_control_handle : RefCell < Option < FanControlHandle >> ,
}
impl AmdGpuController {
2025-01-12 17:16:02 +02:00
#[allow(unused_variables)]
2024-12-18 23:55:31 +02:00
pub fn new_from_path (
2025-01-12 17:16:02 +02:00
common : CommonControllerInfo ,
libdrm_amdgpu : Option <& LibDrmAmdgpu > ,
2024-12-18 23:55:31 +02:00
) -> anyhow ::Result < Self > {
2025-01-12 17:16:02 +02:00
let handle = GpuHandle ::new_from_path ( common . sysfs_path . clone ())
2024-10-25 23:35:26 +03:00
. map_err ( | error | anyhow! ( "failed to initialize gpu handle: {error}" )) ? ;
2025-01-12 17:16:02 +02:00
#[allow(unused_mut)]
2024-10-25 23:35:26 +03:00
let mut drm_handle = None ;
2025-01-12 17:16:02 +02:00
#[cfg(not(test))]
2025-02-05 14:36:31 +02:00
if let Some ( libdrm_amdgpu ) = libdrm_amdgpu {
if handle . get_driver () == "amdgpu" {
drm_handle = Some (
get_drm_handle ( & handle , libdrm_amdgpu )
. context ( "Could not get AMD DRM handle" ) ? ,
);
2024-10-25 23:35:26 +03:00
}
}
Ok ( Self {
handle ,
drm_handle ,
2025-01-12 17:16:02 +02:00
common ,
2024-10-25 23:35:26 +03:00
fan_control_handle : RefCell ::new ( None ),
})
}
fn hw_mon_and_then < U > ( & self , f : fn ( & HwMon ) -> Result < U , Error > ) -> Option < U > {
self . handle . hw_monitors . first (). and_then ( | mon | f ( mon ). ok ())
}
fn hw_mon_map < U > ( & self , f : fn ( & HwMon ) -> U ) -> Option < U > {
self . handle . hw_monitors . first (). map ( f )
}
2025-03-30 15:39:10 +03:00
async fn set_static_fan_control ( & self , static_speed : f32 ) -> anyhow ::Result < Vec < CommitHandle >> {
2024-10-25 23:35:26 +03:00
// Stop existing task to set static speed
self . stop_fan_control ( false ). await ? ;
2025-03-30 15:39:10 +03:00
let mut commit_handles = Vec ::new ();
2024-10-25 23:35:26 +03:00
// Use PMFW curve functionality for static speed when it is available
if let Ok ( current_curve ) = self . handle . get_fan_curve () {
2025-03-16 18:40:03 +02:00
if let Ok ( true ) = self . handle . get_fan_zero_rpm_enable () {
2025-03-30 15:39:10 +03:00
match self . handle . set_fan_zero_rpm_enable ( false ) {
Ok ( zero_rpm_commit ) => {
commit_handles . push ( zero_rpm_commit );
}
Err ( err ) => {
error! ( "could not disable zero RPM mode for static fan control: {err}" );
}
2025-03-16 18:40:03 +02:00
}
}
2024-10-25 23:35:26 +03:00
let allowed_ranges = current_curve . allowed_ranges . clone (). ok_or_else ( || {
anyhow! ( "The GPU does not allow setting custom fan values (is overdrive enabled?)" )
}) ? ;
let min_temperature = allowed_ranges . temperature_range . start ();
let max_temperature = allowed_ranges . temperature_range . end ();
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2025-03-20 21:42:07 +02:00
let custom_pwm = ( f32 ::from ( * allowed_ranges . speed_range . end ()) * static_speed ) as u8 ;
2024-10-25 23:35:26 +03:00
let static_pwm = cmp ::max ( * allowed_ranges . speed_range . start (), custom_pwm );
let mut points = vec! [( * min_temperature , static_pwm )];
for _ in 1 .. current_curve . points . len () {
points . push (( * max_temperature , static_pwm ));
}
let new_curve = PmfwCurve {
points : points . into_boxed_slice (),
allowed_ranges : Some ( allowed_ranges ),
};
debug! ( "setting static curve {new_curve:?}" );
2025-03-30 15:39:10 +03:00
let curve_commit = self
2024-10-25 23:35:26 +03:00
. handle
. set_fan_curve ( & new_curve )
. context ( "Could not set fan curve" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push ( curve_commit );
2024-10-25 23:35:26 +03:00
2025-03-30 15:39:10 +03:00
Ok ( commit_handles )
2024-10-25 23:35:26 +03:00
} else {
let hw_mon = self
. handle
. hw_monitors
. first ()
. cloned ()
. context ( "This GPU has no monitor" ) ? ;
hw_mon
. set_fan_control_method ( FanControlMethod ::Manual )
. context ( "Could not set fan control method" ) ? ;
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2025-03-20 21:42:07 +02:00
let static_pwm = ( f32 ::from ( u8 ::MAX ) * static_speed ) as u8 ;
2024-10-25 23:35:26 +03:00
hw_mon
. set_fan_pwm ( static_pwm )
. context ( "could not set fan speed" ) ? ;
debug! ( "set fan speed to {}" , static_speed );
2025-03-30 15:39:10 +03:00
Ok ( vec! [])
2024-10-25 23:35:26 +03:00
}
}
async fn start_curve_fan_control (
& self ,
curve : FanCurve ,
settings : FanControlSettings ,
) -> anyhow ::Result < Option < CommitHandle >> {
// Use the PMFW curve functionality when it is available
// Otherwise, fall back to manual fan control via a task
if let Ok ( current_curve ) = self . handle . get_fan_curve () {
let new_curve = curve
. into_pmfw_curve ( current_curve . clone ())
. context ( "Invalid fan curve" ) ? ;
debug! ( "setting pmfw curve {new_curve:?}" );
let commit_handle = self
. handle
. set_fan_curve ( & new_curve )
. context ( "Could not set fan curve" ) ? ;
Ok ( Some ( commit_handle ))
} else {
self . start_curve_fan_control_task ( curve , settings ). await ? ;
Ok ( None )
}
}
async fn start_curve_fan_control_task (
& self ,
curve : FanCurve ,
settings : FanControlSettings ,
) -> anyhow ::Result < () > {
// Stop existing task to re-apply new curve
self . stop_fan_control ( false ). await ? ;
let hw_mon = self
. handle
. hw_monitors
. first ()
. cloned ()
. context ( "This GPU has no monitor" ) ? ;
2024-12-26 13:00:44 +02:00
let temps = hw_mon . get_temps ();
match temps . len () {
0 => return Err ( anyhow! ( "GPU has no temperature reporting" )),
1 => {
warn! ( "GPU has only one temperature sensor, 'temperature_key' setting will be ignored" );
}
2025-03-01 09:56:10 +02:00
_ => {
if ! temps . contains_key ( & settings . temperature_key ) {
return Err ( anyhow! (
"Sensor with name {} not found, available sensors: {}" ,
settings . temperature_key ,
temps
. keys ()
. map ( String ::as_str )
. collect ::< Vec <& str >> ()
. join ( "," )
));
}
}
2024-12-26 13:00:44 +02:00
}
2024-10-25 23:35:26 +03:00
hw_mon
. set_fan_control_method ( FanControlMethod ::Manual )
. context ( "Could not set fan control method" ) ? ;
let mut notify_guard = self
. fan_control_handle
. try_borrow_mut ()
. map_err ( | err | anyhow! ( "Lock error: {err}" )) ? ;
let notify = Rc ::new ( Notify ::new ());
let task_notify = notify . clone ();
debug! ( "spawning new fan control task" );
let handle = tokio ::task ::spawn_local ( async move {
let mut last_pwm = ( None , Instant ::now ());
let mut last_temp = 0.0 ;
// If the fan speed could was able to be set at least once
let mut control_available = false ;
let temp_key = settings . temperature_key . clone ();
let interval = Duration ::from_millis ( settings . interval_ms );
let spindown_delay = Duration ::from_millis ( settings . spindown_delay_ms . unwrap_or ( 0 ));
#[allow(clippy::cast_precision_loss)]
let change_threshold = settings . change_threshold . unwrap_or ( 0 ) as f32 ;
2025-03-01 09:56:10 +02:00
let mut retries = 0 ;
2024-10-25 23:35:26 +03:00
loop {
select! {
() = sleep ( interval ) => (),
() = task_notify . notified () => break ,
}
let mut temps = hw_mon . get_temps ();
2024-12-26 13:00:44 +02:00
let temp = if temps . len () == 1 {
temps . into_values (). next (). unwrap ()
2025-03-01 09:56:10 +02:00
} else if let Some ( value ) = temps . remove ( & temp_key ) {
value
2024-12-26 13:00:44 +02:00
} else {
2025-03-01 09:56:10 +02:00
retries += 1 ;
if retries == FAN_CONTROL_RETRIES {
error! ( "could not get temperature sensor {temp_key}, exiting fan control (reached max attempts)" );
break ;
}
error! ( "could not get temperature sensor {temp_key} from {} sensors (assuming error is temporary, attempt {retries}/{FAN_CONTROL_RETRIES})" , temps . len ());
continue ;
2024-12-26 13:00:44 +02:00
};
2024-10-25 23:35:26 +03:00
let current_temp = temp . current . expect ( "Missing temp" );
if ( last_temp - current_temp ). abs () < change_threshold {
trace! ( "temperature changed from {last_temp}°C to {current_temp}°C, which is less than the {change_threshold}°C threshold, skipping speed adjustment" );
continue ;
}
let target_pwm = curve . pwm_at_temp ( temp );
let now = Instant ::now ();
if let ( Some ( previous_pwm ), previous_timestamp ) = last_pwm {
let diff = now - previous_timestamp ;
if target_pwm < previous_pwm && diff < spindown_delay {
trace! (
"delaying fan spindown ({}ms left)" ,
( spindown_delay - diff ). as_millis ()
);
continue ;
}
}
last_pwm = ( Some ( target_pwm ), now );
last_temp = current_temp ;
trace! ( "fan control tick: setting pwm to {target_pwm}" );
match hw_mon . set_fan_pwm ( target_pwm ) {
Ok (()) => control_available = true ,
Err ( err ) => {
error! ( "could not set fan speed: {err}" );
if control_available {
2025-03-15 13:37:35 +02:00
retries += 1 ;
if retries == FAN_CONTROL_RETRIES {
error! ( "could not set fan speed after {retries} attempts, exiting fan control (reached max attempts)" );
break ;
}
info! ( "fan control was previously available, assuming the error is temporary (attempt {retries}/{FAN_CONTROL_RETRIES})" );
if ! matches! (
hw_mon . get_fan_control_method (),
Ok ( FanControlMethod ::Manual ),
) {
info! ( "fan control method was changed externally, setting back to manual" );
if let Err ( err ) =
hw_mon . set_fan_control_method ( FanControlMethod ::Manual )
{
error! ( "could not set fan control back to manual: {err}" );
break ;
}
}
2024-10-25 23:35:26 +03:00
} else {
info! ( "disabling fan control" );
break ;
}
}
}
2025-03-15 13:37:35 +02:00
retries = 0 ;
2024-10-25 23:35:26 +03:00
}
debug! ( "exited fan control task" );
2025-03-15 13:37:35 +02:00
if let Err ( err ) = hw_mon . set_fan_control_method ( FanControlMethod ::Auto ) {
error! ( "could not reset fan control back to auto: {err}" );
}
2024-10-25 23:35:26 +03:00
});
* notify_guard = Some (( notify , handle ));
debug! (
"started fan control with interval {}ms" ,
settings . interval_ms
);
Ok (())
}
async fn stop_fan_control ( & self , reset_mode : bool ) -> anyhow ::Result < () > {
let maybe_notify = self
. fan_control_handle
. try_borrow_mut ()
. map_err ( | err | anyhow! ( "Lock error: {err}" )) ?
. take ();
if let Some (( notify , handle )) = maybe_notify {
notify . notify_one ();
handle . await ? ;
}
if reset_mode {
if self . handle . get_fan_curve (). is_ok () {
if let Err ( err ) = self . handle . reset_fan_curve () {
warn! ( "could not reset fan curve: {err:#}" );
}
}
if let Some ( hw_mon ) = self . handle . hw_monitors . first (). cloned () {
if let Ok ( current_control ) = hw_mon . get_fan_control_method () {
if ! matches! ( current_control , FanControlMethod ::Auto ) {
hw_mon
. set_fan_control_method ( FanControlMethod ::Auto )
. context ( "Could not set fan control back to automatic" ) ? ;
}
}
}
}
Ok (())
}
fn get_power_states_kind (
& self ,
2025-04-14 21:19:50 +03:00
gpu_config : Option <& GpuConfig > ,
2024-10-25 23:35:26 +03:00
kind : PowerLevelKind ,
attempt : u32 ,
) -> Vec < PowerState > {
let enabled_states = gpu_config . and_then ( | gpu | gpu . power_states . get ( & kind ));
let levels = self
. handle
. get_clock_levels ( kind )
. unwrap_or_else ( | _ | PowerLevels {
levels : Vec ::new (),
active : None ,
})
. levels ;
if attempt < MAX_PSTATE_READ_ATTEMPTS
&& levels . iter (). any ( | value | * value >= u64 ::from ( u16 ::MAX ))
{
debug! ( "GPU reported nonsensical p-state value, retrying" );
return self . get_power_states_kind ( gpu_config , kind , attempt + 1 );
}
levels
. into_iter ()
. enumerate ()
. map ( | ( i , value ) | {
let i = u8 ::try_from ( i ). unwrap ();
2025-04-23 22:11:56 +03:00
let enabled = enabled_states . is_none_or ( | enabled | enabled . contains ( & i ));
2024-10-25 23:35:26 +03:00
PowerState {
enabled ,
min_value : None ,
value ,
index : Some ( i ),
}
})
. collect ()
}
fn first_hw_mon ( & self ) -> anyhow ::Result <& HwMon > {
self . handle
. hw_monitors
. first ()
. context ( "GPU has no hardware monitor" )
}
2025-02-01 11:21:02 +02:00
fn get_clockspeed ( & self ) -> ClockspeedStats {
let vram_clockspeed = self
. drm_handle
. as_ref ()
. and_then ( | handle | handle . sensor_info ( SENSOR_TYPE ::GFX_MCLK ). ok ())
. map ( u64 ::from )
. or_else ( || self . hw_mon_and_then ( HwMon ::get_vram_clockspeed ));
ClockspeedStats {
gpu_clockspeed : self . hw_mon_and_then ( HwMon ::get_gpu_clockspeed ),
current_gfxclk : self . get_current_gfxclk (),
vram_clockspeed ,
}
}
2025-01-12 17:16:02 +02:00
fn get_current_gfxclk ( & self ) -> Option < u64 > {
2024-10-25 23:35:26 +03:00
self . drm_handle
. as_ref ()
. and_then ( | drm_handle | drm_handle . get_gpu_metrics (). ok ())
. and_then ( | metrics | metrics . get_current_gfxclk ())
2025-01-12 17:16:02 +02:00
. map ( u64 ::from )
2024-10-25 23:35:26 +03:00
}
fn get_full_vbios_version ( & self ) -> Option < String > {
if let Some ( drm_handle ) = & self . drm_handle {
if let Ok ( vbios_info ) = drm_handle . get_vbios_info () {
return Some ( format! ( " {} [ {} ]" , vbios_info . ver , vbios_info . date ));
}
}
self . handle . get_vbios_version (). ok ()
}
fn get_drm_info ( & self ) -> Option < DrmInfo > {
use libdrm_amdgpu_sys ::AMDGPU ::VRAM_TYPE ;
trace! ( "Reading DRM info" );
let drm_handle = self . drm_handle . as_ref ();
let drm_memory_info =
drm_handle
. and_then ( | handle | handle . memory_info (). ok ())
. map ( | memory_info | DrmMemoryInfo {
resizeable_bar : Some ( memory_info . check_resizable_bar ()),
cpu_accessible_used : memory_info . cpu_accessible_vram . heap_usage ,
cpu_accessible_total : memory_info . cpu_accessible_vram . total_heap_size ,
});
match drm_handle {
Some ( handle ) => handle . device_info (). ok (). map ( | drm_info | DrmInfo {
device_name : drm_info . find_device_name (),
pci_revision_id : Some ( drm_info . pci_rev_id ()),
family_name : Some ( drm_info . get_family_name (). to_string ()),
family_id : Some ( drm_info . family_id ()),
asic_name : Some ( drm_info . get_asic_name (). to_string ()),
chip_class : Some ( drm_info . get_chip_class (). to_string ()),
compute_units : Some ( drm_info . cu_active_number ),
2025-04-18 18:12:15 +03:00
isa : drm_info
. get_gfx_target_version ()
. map ( | version | version . to_string ()),
2025-03-15 10:01:58 +02:00
streaming_multiprocessors : None ,
2024-10-25 23:35:26 +03:00
cuda_cores : None ,
vram_type : Some ( drm_info . get_vram_type (). to_string ()),
2025-03-15 10:09:29 +02:00
vram_vendor : self . handle . get_vram_vendor (). ok (),
2024-10-25 23:35:26 +03:00
vram_clock_ratio : match drm_info . get_vram_type () {
VRAM_TYPE ::GDDR6 => 2.0 ,
_ => 1.0 ,
},
vram_bit_width : Some ( drm_info . vram_bit_width ),
vram_max_bw : Some ( drm_info . peak_memory_bw_gb (). to_string ()),
l1_cache_per_cu : Some ( drm_info . get_l1_cache_size ()),
l2_cache : Some ( drm_info . calc_l2_cache_size ()),
l3_cache_mb : Some ( drm_info . calc_l3_cache_size_mb ()),
memory_info : drm_memory_info ,
2025-05-05 18:15:59 +03:00
rop_info : Some ( RopInfo {
unit_count : drm_info . rb_pipes (),
operations_factor : if drm_info . get_asic_name (). rbplus_allowed () {
8
} else {
4
},
operations_count : drm_info . calc_rop_count (),
}),
2025-01-12 17:16:02 +02:00
intel : IntelDrmInfo ::default (),
2024-10-25 23:35:26 +03:00
}),
None => None ,
}
}
fn get_link_info ( & self ) -> LinkInfo {
2025-05-29 22:04:55 +03:00
#[cfg(not(test))]
2025-05-26 06:22:19 +09:00
let gpu_pcie_port_bus = self
. handle
. get_pci_slot_name ()
. and_then ( | name | name . parse ::< PCI ::BUS_INFO > (). ok ())
. map ( | bus_info | bus_info . get_gpu_pcie_port_bus ());
2025-05-29 22:04:55 +03:00
#[cfg(test)]
let gpu_pcie_port_bus : Option < PCI ::BUS_INFO > = None ;
2025-05-26 06:22:19 +09:00
let current_link = gpu_pcie_port_bus . and_then ( | bus | bus . get_current_link_info ());
let max_pcie_link = gpu_pcie_port_bus . and_then ( | bus | bus . get_max_link_info ());
let max_system_link = gpu_pcie_port_bus . and_then ( | bus | bus . get_max_system_link ());
let max_link = if let ( Some ( max_pcie_link ), Some ( max_system_link )) =
( max_pcie_link , max_system_link )
{
Some ( PCI ::LINK {
r#gen : cmp ::min ( max_pcie_link . r#gen , max_system_link . r#gen ),
width : cmp ::min ( max_pcie_link . width , max_system_link . width ),
})
} else {
None
};
2025-05-24 12:42:47 +03:00
2024-10-25 23:35:26 +03:00
LinkInfo {
2025-05-25 10:34:21 +03:00
current_width : current_link
2025-05-24 12:42:47 +03:00
. map ( | link | link . width . to_string ())
2025-05-25 10:34:21 +03:00
. or_else ( || self . handle . get_current_link_width (). ok ()),
current_speed : current_link
2025-05-24 12:42:47 +03:00
. map ( | link | format! ( "Gen {} " , link . gen ))
2025-05-25 10:34:21 +03:00
. or_else ( || self . handle . get_current_link_speed (). ok ()),
max_width : max_link
2025-05-26 06:22:19 +09:00
. map ( | link | link . width . to_string ())
2025-05-25 10:34:21 +03:00
. or_else ( || self . handle . get_max_link_width (). ok ()),
max_speed : max_link
2025-05-26 06:22:19 +09:00
. map ( | link | format! ( "Gen {} " , link . gen ))
2025-05-24 12:43:57 +03:00
. or_else ( || self . handle . get_max_link_speed (). ok ()),
2024-10-25 23:35:26 +03:00
}
}
fn get_throttle_info ( & self ) -> Option < BTreeMap < String , Vec < String >>> {
use libdrm_amdgpu_sys ::AMDGPU ::ThrottlerType ;
self . drm_handle
. as_ref ()
. and_then ( | drm_handle | drm_handle . get_gpu_metrics (). ok ())
2025-05-16 17:35:58 +03:00
. and_then ( | metrics | metrics . get_throttle_status_info ())
. map ( | throttle | {
2024-11-07 09:01:36 +02:00
let mut grouped_bits : HashMap < ThrottlerType , HashSet < u8 >> = HashMap ::new ();
2024-10-25 23:35:26 +03:00
for bit in throttle . get_all_throttler () {
let throttle_type = ThrottlerType ::from ( bit );
2024-11-07 09:01:36 +02:00
grouped_bits
. entry ( throttle_type )
2024-10-25 23:35:26 +03:00
. or_default ()
2024-11-07 09:01:36 +02:00
. insert ( bit as u8 );
2024-10-25 23:35:26 +03:00
}
2024-11-07 09:01:36 +02:00
grouped_bits
. into_iter ()
. map ( | ( throttle_type , bits ) | {
let mut names : Vec < String > = bits
. into_iter ()
. map ( | bit | ThrottlerBit ::from ( bit ). to_string ())
. collect ();
names . sort_unstable ();
( throttle_type . to_string (), names )
})
. collect ()
2024-10-25 23:35:26 +03:00
})
}
fn debugfs_path ( & self ) -> Option < PathBuf > {
let slot_id = self . handle . get_pci_slot_name () ? ;
let name_search_term = format! ( "dev= {slot_id} " );
for entry in fs ::read_dir ( "/sys/kernel/debug/dri" ). ok () ? . flatten () {
let debugfs_path = entry . path ();
let name_file_path = debugfs_path . join ( "name" );
if name_file_path . exists () {
if let Ok ( contents ) = fs ::read_to_string ( & name_file_path ) {
if contents . contains ( & name_search_term ) {
return Some ( debugfs_path );
}
}
}
}
None
}
2024-11-10 21:13:19 +02:00
fn is_steam_deck ( & self ) -> bool {
2025-01-12 17:16:02 +02:00
self . common . pci_info . device_pci_info . vendor_id == VENDOR_AMD
&& STEAM_DECK_IDS . contains ( & self . common . pci_info . device_pci_info . model_id . as_str ())
2024-11-10 21:13:19 +02:00
}
2024-10-25 23:35:26 +03:00
}
impl GpuController for AmdGpuController {
2025-01-12 17:16:02 +02:00
fn controller_info ( & self ) -> & CommonControllerInfo {
& self . common
2024-10-25 23:35:26 +03:00
}
2025-03-14 09:04:19 +02:00
fn get_info ( & self ) -> LocalBoxFuture < '_ , DeviceInfo > {
Box ::pin ( async move {
let vulkan_info = match get_vulkan_info ( & self . common . pci_info ). await {
Ok ( info ) => Some ( info ),
Err ( err ) => {
warn! ( "could not load vulkan info: {err}" );
None
}
};
let pci_info = Some ( self . common . pci_info . clone ());
let driver = self . handle . get_driver (). to_owned ();
let vbios_version = self . get_full_vbios_version ();
let link_info = self . get_link_info ();
let drm_info = self . get_drm_info ();
2025-04-23 22:11:56 +03:00
let opencl_info = get_opencl_info ( & self . common );
2025-03-14 09:04:19 +02:00
DeviceInfo {
pci_info ,
vulkan_info ,
driver ,
vbios_version ,
link_info ,
2025-04-23 22:11:56 +03:00
opencl_info ,
2025-03-14 09:04:19 +02:00
drm_info ,
2024-10-25 23:35:26 +03:00
}
2025-03-14 09:04:19 +02:00
})
2024-10-25 23:35:26 +03:00
}
2025-03-20 21:42:07 +02:00
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
2025-04-14 21:19:50 +03:00
fn get_stats ( & self , gpu_config : Option <& GpuConfig > ) -> DeviceStats {
2025-03-16 18:40:03 +02:00
let metrics = GpuMetrics ::get_from_sysfs_path ( self . handle . get_path ()). ok ();
let metrics = metrics . as_ref ();
2025-03-20 21:42:07 +02:00
let pmfw_curve = self . handle . get_fan_curve (). ok ();
let pmfw_curve_range = pmfw_curve
. as_ref ()
. and_then ( | curve | curve . allowed_ranges . as_ref ())
. map ( | range | & range . speed_range );
let pwm_max = pmfw_curve_range
. map ( | range | * range . end ())
. map ( | percent | ( f64 ::from ( percent ) * 2.55 ) as u32 )
. or_else ( || self . hw_mon_and_then ( HwMon ::get_fan_max_pwm ). map ( u32 ::from ));
let pwm_min = pmfw_curve_range
. map ( | range | * range . start ())
. map ( | percent | ( f64 ::from ( percent ) * 2.55 ) as u32 )
. or_else ( || self . hw_mon_and_then ( HwMon ::get_fan_min_pwm ). map ( u32 ::from ));
2024-10-25 23:35:26 +03:00
let fan_settings = gpu_config . and_then ( | config | config . fan_control_settings . as_ref ());
DeviceStats {
fan : FanStats {
control_enabled : gpu_config . is_some_and ( | config | config . fan_control_enabled ),
control_mode : fan_settings . map ( | settings | settings . mode ),
static_speed : fan_settings . map ( | settings | settings . static_speed ),
curve : fan_settings . map ( | settings | settings . curve . 0. clone ()),
spindown_delay_ms : fan_settings . and_then ( | settings | settings . spindown_delay_ms ),
change_threshold : fan_settings . and_then ( | settings | settings . change_threshold ),
2025-05-11 11:30:30 +03:00
temperature_key : fan_settings . map ( | settings | settings . temperature_key . clone ()),
2025-05-10 16:30:30 +03:00
auto_threshold : None ,
2025-03-16 18:40:03 +02:00
speed_current : self . hw_mon_and_then ( HwMon ::get_fan_current ). or_else ( || {
metrics
. and_then ( MetricsInfo ::get_current_fan_speed )
. map ( u32 ::from )
}),
2024-10-25 23:35:26 +03:00
speed_max : self . hw_mon_and_then ( HwMon ::get_fan_max ),
speed_min : self . hw_mon_and_then ( HwMon ::get_fan_min ),
2025-03-16 18:40:03 +02:00
pwm_current : self . hw_mon_and_then ( HwMon ::get_fan_pwm ). or_else ( || {
metrics
. and_then ( MetricsInfo ::get_fan_pwm )
. and_then ( | pwm | u8 ::try_from ( pwm ). ok ())
}),
2025-03-20 21:42:07 +02:00
pwm_max ,
pwm_min ,
2025-05-09 23:03:02 +03:00
temperature_range : pmfw_curve
. as_ref ()
. and_then ( | curve | curve . allowed_ranges . as_ref ())
. map ( | range | {
(
* range . temperature_range . start (),
* range . temperature_range . end (),
)
}),
2024-10-25 23:35:26 +03:00
pmfw_info : PmfwInfo {
acoustic_limit : self . handle . get_fan_acoustic_limit (). ok (),
acoustic_target : self . handle . get_fan_acoustic_target (). ok (),
target_temp : self . handle . get_fan_target_temperature (). ok (),
minimum_pwm : self . handle . get_fan_minimum_pwm (). ok (),
2024-11-14 00:11:24 +02:00
zero_rpm_enable : self . handle . get_fan_zero_rpm_enable (). ok (),
zero_rpm_temperature : self . handle . get_fan_zero_rpm_stop_temperature (). ok (),
2024-10-25 23:35:26 +03:00
},
},
2025-02-01 11:21:02 +02:00
clockspeed : self . get_clockspeed (),
2024-10-25 23:35:26 +03:00
voltage : VoltageStats {
gpu : self . hw_mon_and_then ( HwMon ::get_gpu_voltage ),
northbridge : self . hw_mon_and_then ( HwMon ::get_northbridge_voltage ),
},
vram : VramStats {
total : self . handle . get_total_vram (). ok (),
used : self . handle . get_used_vram (). ok (),
},
power : PowerStats {
average : self . hw_mon_and_then ( HwMon ::get_power_average ),
current : self . hw_mon_and_then ( HwMon ::get_power_input ),
cap_current : self . hw_mon_and_then ( HwMon ::get_power_cap ),
cap_max : self . hw_mon_and_then ( HwMon ::get_power_cap_max ),
cap_min : self . hw_mon_and_then ( HwMon ::get_power_cap_min ),
cap_default : self . hw_mon_and_then ( HwMon ::get_power_cap_default ),
},
temps : self . hw_mon_map ( HwMon ::get_temps ). unwrap_or_default (),
busy_percent : self . handle . get_busy_percent (). ok (),
performance_level : self . handle . get_power_force_performance_level (). ok (),
core_power_state : self
. handle
. get_core_clock_levels ()
. ok ()
. and_then ( | levels | levels . active ),
memory_power_state : self
. handle
. get_memory_clock_levels ()
. ok ()
. and_then ( | levels | levels . active ),
pcie_power_state : self
. handle
. get_pcie_clock_levels ()
. ok ()
. and_then ( | levels | levels . active ),
throttle_info : self . get_throttle_info (),
}
}
2025-05-29 22:04:55 +03:00
fn get_clocks_info ( & self , _gpu_config : Option <& GpuConfig > ) -> anyhow ::Result < ClocksInfo > {
let clocks_table = self
2024-10-25 23:35:26 +03:00
. handle
. get_clocks_table ()
. context ( "Clocks table not available" ) ? ;
2025-03-16 18:40:03 +02:00
2024-10-25 23:35:26 +03:00
Ok ( clocks_table . into ())
}
2025-04-14 21:19:50 +03:00
fn get_power_states ( & self , gpu_config : Option <& GpuConfig > ) -> PowerStates {
2024-10-25 23:35:26 +03:00
let core = self . get_power_states_kind ( gpu_config , PowerLevelKind ::CoreClock , 0 );
let vram = self . get_power_states_kind ( gpu_config , PowerLevelKind ::MemoryClock , 0 );
PowerStates { core , vram }
}
fn get_power_profile_modes ( & self ) -> anyhow ::Result < PowerProfileModesTable > {
Ok ( self . handle . get_power_profile_modes () ? )
}
fn reset_pmfw_settings ( & self ) {
let handle = & self . handle ;
if self . handle . get_fan_target_temperature (). is_ok () {
if let Err ( err ) = handle . reset_fan_target_temperature () {
warn! ( "Could not reset target temperature: {err:#}" );
}
}
if self . handle . get_fan_acoustic_target (). is_ok () {
if let Err ( err ) = handle . reset_fan_acoustic_target () {
warn! ( "Could not reset acoustic target: {err:#}" );
}
}
if self . handle . get_fan_acoustic_limit (). is_ok () {
if let Err ( err ) = handle . reset_fan_acoustic_limit () {
warn! ( "Could not reset acoustic limit: {err:#}" );
}
}
if self . handle . get_fan_minimum_pwm (). is_ok () {
if let Err ( err ) = handle . reset_fan_minimum_pwm () {
warn! ( "Could not reset minimum pwm: {err:#}" );
}
}
}
fn vbios_dump ( & self ) -> anyhow ::Result < Vec < u8 >> {
let debugfs = self . debugfs_path (). context ( "DebugFS not found" ) ? ;
fs ::read ( debugfs . join ( "amdgpu_vbios" )). context ( "Could not read VBIOS file" )
}
#[allow(clippy::too_many_lines)]
2025-04-14 21:19:50 +03:00
fn apply_config < 'a > ( & 'a self , config : & 'a GpuConfig ) -> LocalBoxFuture < 'a , anyhow ::Result < () >> {
2024-10-25 23:35:26 +03:00
Box ::pin ( async {
2025-03-30 15:39:10 +03:00
let mut commit_handles = VecDeque ::new ();
2024-10-25 23:35:26 +03:00
// Reset the clocks table in case the settings get reverted back to not having a clocks value configured
self . handle . reset_clocks_table (). ok ();
2025-03-30 15:39:10 +03:00
if ! config . fan_control_enabled {
self . stop_fan_control ( true )
. await
. context ( "Failed to stop fan control" ) ? ;
}
2024-11-10 21:13:19 +02:00
if self . is_steam_deck () {
// Van Gogh/Sephiroth only allow clock settings to be used with manual performance mode
self . handle
. set_power_force_performance_level ( PerformanceLevel ::Manual )
. ok ();
}
2024-10-25 23:35:26 +03:00
if config . is_core_clocks_used () {
2025-03-04 20:15:57 +02:00
match self . handle . get_clocks_table () {
Ok ( original_table ) => {
let mut table = original_table . clone ();
2025-04-14 21:19:50 +03:00
apply_clocks_config_to_table ( & config . clocks_configuration , & mut table )
2025-03-04 20:15:57 +02:00
. context ( "Failed to apply clocks configuration to table" ) ? ;
debug! (
"writing clocks commands: {:#?}" ,
table
. get_commands ( & original_table )
. context ( "Failed to get table commands" ) ?
);
2024-10-25 23:35:26 +03:00
2025-03-04 20:15:57 +02:00
let handle = self
. handle
. set_clocks_table ( & table )
. context ( "Could not write clocks table" )
. with_context ( || {
format! (
"Clocks table commands: {:?} " ,
table . get_commands ( & original_table )
)
}) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_back ( handle );
2025-03-04 20:15:57 +02:00
}
Err ( err ) => {
2025-03-16 18:40:03 +02:00
error! ( "custom clock settings are present but will be ignored, could not get clocks table: {err}" );
2025-03-04 20:15:57 +02:00
}
}
2024-10-25 23:35:26 +03:00
}
2025-03-30 15:39:10 +03:00
match self . handle . get_power_force_performance_level () {
Ok ( _ ) => {
let performance_level =
config . performance_level . unwrap_or ( PerformanceLevel ::Auto );
self . handle
. set_power_force_performance_level ( performance_level )
. context ( "Failed to set power performance level" ) ? ;
}
Err ( err ) => {
error! ( "could not get current performance level: {err}" );
}
2024-10-25 23:35:26 +03:00
}
if let Some ( mode_index ) = config . power_profile_mode_index {
if config . performance_level != Some ( PerformanceLevel ::Manual ) {
return Err ( anyhow! (
"Performance level has to be set to `manual` to use power profile modes"
));
}
if config . custom_power_profile_mode_hueristics . is_empty () {
self . handle
. set_active_power_profile_mode ( mode_index )
. context ( "Failed to set active power profile mode" ) ? ;
} else {
self . handle
. set_custom_power_profile_mode_heuristics (
& config . custom_power_profile_mode_hueristics ,
)
. context ( "Failed to set custom power profile mode heuristics" ) ? ;
}
}
if config . fan_control_enabled {
if let Some ( ref settings ) = config . fan_control_settings {
match settings . mode {
lact_schema ::FanControlMode ::Static => {
2025-03-30 15:39:10 +03:00
let fan_handles = self
2024-10-25 23:35:26 +03:00
. set_static_fan_control ( settings . static_speed )
. await
2025-03-30 15:39:10 +03:00
. context ( "Failed to set static fan control" ) ? ;
for handle in fan_handles {
commit_handles . push_front ( handle );
2024-10-25 23:35:26 +03:00
}
}
lact_schema ::FanControlMode ::Curve => {
if settings . curve . 0. is_empty () {
return Err ( anyhow! ( "Cannot use empty fan curve" ));
}
if let Some ( commit_handle ) = self
. start_curve_fan_control ( settings . curve . clone (), settings . clone ())
. await
. context ( "Failed to set curve fan control" ) ?
{
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2024-10-25 23:35:26 +03:00
}
}
}
} else {
return Err ( anyhow! (
"Trying to enable fan control with no settings provided"
));
}
} else {
let pmfw = & config . pmfw_options ;
if let Some ( acoustic_limit ) = pmfw . acoustic_limit {
if self
. handle
. get_fan_acoustic_limit ()
. context ( "Could not get acoustic limit" ) ?
. current
!= acoustic_limit
{
let commit_handle = self
. handle
. set_fan_acoustic_limit ( acoustic_limit )
. context ( "Could not set acoustic limit" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2024-10-25 23:35:26 +03:00
}
}
if let Some ( acoustic_target ) = pmfw . acoustic_target {
if self
. handle
. get_fan_acoustic_target ()
. context ( "Could not get acoustic target" ) ?
. current
!= acoustic_target
{
let commit_handle = self
. handle
. set_fan_acoustic_target ( acoustic_target )
. context ( "Could not set acoustic target" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2024-10-25 23:35:26 +03:00
}
}
if let Some ( target_temperature ) = pmfw . target_temperature {
if self
. handle
. get_fan_target_temperature ()
. context ( "Could not get target temperature" ) ?
. current
!= target_temperature
{
let commit_handle = self
. handle
. set_fan_target_temperature ( target_temperature )
. context ( "Could not set target temperature" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2024-10-25 23:35:26 +03:00
}
}
if let Some ( minimum_pwm ) = pmfw . minimum_pwm {
if self
. handle
. get_fan_minimum_pwm ()
. context ( "Could not get minimum pwm" ) ?
. current
!= minimum_pwm
{
let commit_handle = self
. handle
. set_fan_minimum_pwm ( minimum_pwm )
. context ( "Could not set minimum pwm" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2024-10-25 23:35:26 +03:00
}
}
}
2024-11-14 00:11:24 +02:00
// Unlike the other PMFW options, zero rpm should be functional with a custom curve
if let Some ( zero_rpm ) = config . pmfw_options . zero_rpm {
2025-03-16 18:40:03 +02:00
match self . handle . get_fan_zero_rpm_enable () {
Ok ( current_zero_rpm ) => {
if current_zero_rpm != zero_rpm {
let commit_handle = self
. handle
. set_fan_zero_rpm_enable ( zero_rpm )
. context ( "Could not set zero RPM mode" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2025-03-16 18:40:03 +02:00
}
}
Err ( err ) => {
error! ( "zero RPM is present in the config, but not available on the GPU: {err}" );
}
2024-11-14 00:11:24 +02:00
}
}
if let Some ( zero_rpm_threshold ) = config . pmfw_options . zero_rpm_threshold {
2025-03-16 18:40:03 +02:00
match self . handle . get_fan_zero_rpm_stop_temperature () {
Ok ( current_threshold ) => {
if current_threshold . current != zero_rpm_threshold {
let commit_handle = self
. handle
. set_fan_zero_rpm_stop_temperature ( zero_rpm_threshold )
. context ( "Could not set zero RPM temperature" ) ? ;
2025-03-30 15:39:10 +03:00
commit_handles . push_front ( commit_handle );
2025-03-16 18:40:03 +02:00
}
}
Err ( err ) => {
error! ( "zero RPM threshold is present in the config, but not available on the GPU: {err}" );
}
2024-11-14 00:11:24 +02:00
}
}
2025-03-30 15:39:10 +03:00
if let Some ( configured_cap ) = config . power_cap {
let hw_mon = self . first_hw_mon () ? ;
hw_mon
. set_power_cap ( configured_cap )
. with_context ( || format! ( "Failed to set power cap: {configured_cap} " )) ? ;
} else if let Ok ( hw_mon ) = self . first_hw_mon () {
if let Ok ( default_cap ) = hw_mon . get_power_cap_default () {
if Ok ( default_cap ) != hw_mon . get_power_cap () {
hw_mon . set_power_cap ( default_cap ). with_context ( || {
format! ( "Failed to set power cap to default cap: {default_cap} " )
}) ? ;
}
}
}
2024-10-25 23:35:26 +03:00
for handle in commit_handles {
handle . commit () ? ;
}
2024-12-31 19:03:39 +02:00
for ( kind , states ) in & config . power_states {
if config . performance_level != Some ( PerformanceLevel ::Manual ) {
return Err ( anyhow! (
"Performance level has to be set to `manual` to configure power states"
));
}
self . handle
. set_enabled_power_levels ( * kind , states )
. with_context ( || format! ( "Could not set {kind:?} power states" )) ? ;
}
2024-10-25 23:35:26 +03:00
Ok (())
})
}
2025-03-15 13:37:35 +02:00
fn reset_clocks ( & self ) -> anyhow ::Result < () > {
2024-10-25 23:35:26 +03:00
if self . handle . get_clocks_table (). is_err () {
return Ok (());
}
self . handle . reset_clocks_table () ? ;
Ok (())
}
2025-03-15 13:37:35 +02:00
fn cleanup ( & self ) -> LocalBoxFuture < '_ , () > {
async {
if let Some (( fan_notify , fan_handle )) = self . fan_control_handle . take () {
debug! ( "sending stop notification to old fan control task" );
fan_notify . notify_one ();
fan_handle . await . unwrap ();
debug! ( "finished controller cleanup" );
}
}
. boxed_local ()
}
2024-10-25 23:35:26 +03:00
}
2025-01-12 17:16:02 +02:00
#[cfg(not(test))]
2025-01-04 22:04:07 +09:00
fn get_drm_handle ( handle : & GpuHandle , libdrm_amdgpu : & LibDrmAmdgpu ) -> anyhow ::Result < DrmHandle > {
2025-01-12 17:16:02 +02:00
use std ::os ::unix ::io ::IntoRawFd ;
2024-10-25 23:35:26 +03:00
let slot_name = handle
. get_pci_slot_name ()
. context ( "Device has no PCI slot name" ) ? ;
let path = format! ( "/dev/dri/by-path/pci- {slot_name} -render" );
2025-01-12 17:16:02 +02:00
let drm_file = fs ::OpenOptions ::new ()
2024-10-25 23:35:26 +03:00
. read ( true )
. write ( true )
. open ( & path )
. with_context ( || format! ( "Could not open drm file at {path} " )) ? ;
2025-01-04 22:04:07 +09:00
let ( handle , _ , _ ) = libdrm_amdgpu
. init_device_handle ( drm_file . into_raw_fd ())
2024-10-25 23:35:26 +03:00
. map_err ( | err | anyhow! ( "Could not open drm handle, error code {err}" )) ? ;
Ok ( handle )
}
2025-04-14 21:19:50 +03:00
fn apply_clocks_config_to_table (
config : & ClocksConfiguration ,
table : & mut ClocksTableGen ,
) -> anyhow ::Result < () > {
2025-05-29 22:04:55 +03:00
if let ClocksTableGen ::Rdna ( ref mut table ) = table {
2025-04-14 21:19:50 +03:00
// Avoid writing settings to the clocks table except the user-specified ones
// There is an issue on some GPU models where the default values are actually outside of the allowed range
// See https://github.com/sibradzic/amdgpu-clocks/issues/32#issuecomment-829953519 (part 2) for an example
table . clear ();
// Normalize the VDDC curve - make sure all of the values are within the allowed range
table . normalize_vddc_curve ();
match config . voltage_offset {
Some ( offset ) => table . set_voltage_offset ( offset ) ? ,
None => table . voltage_offset = None ,
2024-10-25 23:35:26 +03:00
}
2025-04-14 21:19:50 +03:00
if let Some ( offset ) = config . gpu_clock_offsets . get ( & 0 ) {
table . sclk_offset = Some ( * offset );
2024-10-25 23:35:26 +03:00
}
2025-04-14 21:19:50 +03:00
}
2024-10-25 23:35:26 +03:00
2025-04-14 21:19:50 +03:00
if let Some ( min_clockspeed ) = config . min_core_clock {
table . set_min_sclk ( min_clockspeed ) ? ;
}
if let Some ( min_clockspeed ) = config . min_memory_clock {
table . set_min_mclk ( min_clockspeed ) ? ;
}
if let Some ( min_voltage ) = config . min_voltage {
table . set_min_voltage ( min_voltage ) ? ;
}
2024-10-25 23:35:26 +03:00
2025-04-14 21:19:50 +03:00
if let Some ( clockspeed ) = config . max_core_clock {
table . set_max_sclk ( clockspeed ) ? ;
}
if let Some ( clockspeed ) = config . max_memory_clock {
table . set_max_mclk ( clockspeed ) ? ;
2024-10-25 23:35:26 +03:00
}
2025-04-14 21:19:50 +03:00
if let Some ( voltage ) = config . max_voltage {
table . set_max_voltage ( voltage ) ? ;
}
Ok (())
2024-10-25 23:35:26 +03:00
}