use std::ffi::c_int;
#[cfg(target_os = "windows")]
use std::ffi::c_void;
pub const ADL_MAX_PATH: usize = 256;
pub const ADL_PMLOG_MAX_SENSORS: usize = 256;
#[cfg(target_os = "windows")]
pub const ADL_OK: c_int = 0;
#[cfg(target_os = "windows")]
pub type AdlContextHandle = *mut c_void;
#[cfg(target_os = "windows")]
pub type AdlMainMallocCallback = unsafe extern "system" fn(c_int) -> *mut c_void;
#[repr(C)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct AdlSingleSensorData {
pub supported: c_int,
pub value: c_int,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct AdlPmLogDataOutput {
pub ul_size: c_int,
pub sensors: [AdlSingleSensorData; ADL_PMLOG_MAX_SENSORS],
}
impl Default for AdlPmLogDataOutput {
fn default() -> Self {
Self {
ul_size: 0,
sensors: [AdlSingleSensorData::default(); ADL_PMLOG_MAX_SENSORS],
}
}
}
const _: () = assert!(
std::mem::size_of::<AdlSingleSensorData>() == 8,
"ADLSingleSensorData must be two 32-bit ints"
);
const _: () = assert!(
std::mem::align_of::<AdlSingleSensorData>() == 4,
"ADLSingleSensorData must be 4-byte aligned, or the sensor array strides wrong"
);
const _: () = assert!(
std::mem::size_of::<AdlPmLogDataOutput>() == 4 + 8 * ADL_PMLOG_MAX_SENSORS,
"ADLPMLogDataOutput must be a leading int followed by a packed 256-entry sensor array"
);
const _: () = assert!(
std::mem::size_of::<AdlPmLogDataOutput>() == 2052,
"ADLPMLogDataOutput is 2052 bytes in AMD's headers"
);
#[repr(C)]
#[derive(Clone, Copy)]
pub struct AdlPmLogDataBuffer {
pub output: AdlPmLogDataOutput,
_headroom: [AdlSingleSensorData; ADL_PMLOG_MAX_SENSORS * 3],
}
impl Default for AdlPmLogDataBuffer {
fn default() -> Self {
Self {
output: AdlPmLogDataOutput::default(),
_headroom: [AdlSingleSensorData::default(); ADL_PMLOG_MAX_SENSORS * 3],
}
}
}
impl AdlPmLogDataBuffer {
pub fn validated(&self) -> Option<&AdlPmLogDataOutput> {
let reported = self.output.ul_size;
if reported == 0 || reported as usize == std::mem::size_of::<AdlPmLogDataOutput>() {
return Some(&self.output);
}
None
}
}
const _: () = assert!(
std::mem::size_of::<AdlPmLogDataBuffer>() > std::mem::size_of::<AdlPmLogDataOutput>(),
"the headroom must actually extend the allocation"
);
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdapterInfo {
pub i_size: c_int,
pub i_adapter_index: c_int,
pub str_udid: [u8; ADL_MAX_PATH],
pub i_bus_number: c_int,
pub i_device_number: c_int,
pub i_function_number: c_int,
pub i_vendor_id: c_int,
pub str_adapter_name: [u8; ADL_MAX_PATH],
pub str_display_name: [u8; ADL_MAX_PATH],
pub i_present: c_int,
pub i_exist: c_int,
pub str_driver_path: [u8; ADL_MAX_PATH],
pub str_driver_path_ext: [u8; ADL_MAX_PATH],
pub str_pnp_string: [u8; ADL_MAX_PATH],
pub i_os_display_index: c_int,
}
impl Default for AdapterInfo {
fn default() -> Self {
Self {
i_size: 0,
i_adapter_index: 0,
str_udid: [0; ADL_MAX_PATH],
i_bus_number: 0,
i_device_number: 0,
i_function_number: 0,
i_vendor_id: 0,
str_adapter_name: [0; ADL_MAX_PATH],
str_display_name: [0; ADL_MAX_PATH],
i_present: 0,
i_exist: 0,
str_driver_path: [0; ADL_MAX_PATH],
str_driver_path_ext: [0; ADL_MAX_PATH],
str_pnp_string: [0; ADL_MAX_PATH],
i_os_display_index: 0,
}
}
}
const _: () = assert!(
std::mem::size_of::<AdapterInfo>() == 1572,
"AdapterInfo (Windows layout) is 1572 bytes in AMD's headers"
);
const _: () = assert!(
std::mem::align_of::<AdapterInfo>() == 4,
"AdapterInfo must be 4-byte aligned, or the adapter array strides wrong"
);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, str_udid) == 8);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, i_bus_number) == 264);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, i_device_number) == 268);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, i_function_number) == 272);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, i_vendor_id) == 276);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, str_adapter_name) == 280);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, str_display_name) == 536);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, i_present) == 792);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, str_pnp_string) == 1312);
const _: () = assert!(std::mem::offset_of!(AdapterInfo, i_os_display_index) == 1568);
pub fn adl_string(bytes: &[u8]) -> Option<&str> {
let len = bytes.iter().position(|&b| b == 0)?;
let prefix = &bytes[..len];
if !prefix.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
return None;
}
std::str::from_utf8(prefix).ok()
}
pub fn adl_string_lossy(bytes: &[u8]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
pub const ADAPTER_POISON_BYTE: u8 = 0xAA;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowState {
Untouched,
Blank,
Populated,
Garbled,
}
impl AdapterInfo {
pub fn poisoned() -> Self {
let poison_int = u32::from_ne_bytes([ADAPTER_POISON_BYTE; 4]) as c_int;
Self {
i_size: poison_int,
i_adapter_index: poison_int,
str_udid: [ADAPTER_POISON_BYTE; ADL_MAX_PATH],
i_bus_number: poison_int,
i_device_number: poison_int,
i_function_number: poison_int,
i_vendor_id: poison_int,
str_adapter_name: [ADAPTER_POISON_BYTE; ADL_MAX_PATH],
str_display_name: [ADAPTER_POISON_BYTE; ADL_MAX_PATH],
i_present: poison_int,
i_exist: poison_int,
str_driver_path: [ADAPTER_POISON_BYTE; ADL_MAX_PATH],
str_driver_path_ext: [ADAPTER_POISON_BYTE; ADL_MAX_PATH],
str_pnp_string: [ADAPTER_POISON_BYTE; ADL_MAX_PATH],
i_os_display_index: poison_int,
}
}
pub fn is_blank(&self) -> bool {
*self == Self::default()
}
pub fn looks_sane(&self) -> bool {
let size_ok = self.i_size == 0 || self.i_size as usize == std::mem::size_of::<Self>();
let index_ok = (0..256).contains(&self.i_adapter_index);
size_ok
&& index_ok
&& adl_string(&self.str_udid).is_some()
&& adl_string(&self.str_adapter_name).is_some()
&& adl_string(&self.str_display_name).is_some()
&& adl_string(&self.str_pnp_string).is_some()
}
pub fn classify(&self) -> RowState {
if *self == Self::poisoned() {
RowState::Untouched
} else if self.is_blank() {
RowState::Blank
} else if self.looks_sane() {
RowState::Populated
} else {
RowState::Garbled
}
}
}
pub struct AdapterInfoArray {
entries: Vec<AdapterInfo>,
requested: usize,
}
impl AdapterInfoArray {
pub fn for_count(requested: usize) -> Self {
let allocated = requested * 2 + 4;
Self {
entries: vec![AdapterInfo::poisoned(); allocated],
requested,
}
}
pub fn as_mut_ptr(&mut self) -> *mut AdapterInfo {
self.entries.as_mut_ptr()
}
pub fn input_size(&self) -> c_int {
self.requested
.checked_mul(std::mem::size_of::<AdapterInfo>())
.and_then(|bytes| c_int::try_from(bytes).ok())
.unwrap_or(0)
}
pub fn requested_entries(&self) -> &[AdapterInfo] {
&self.entries[..self.requested]
}
pub fn row_states(&self) -> Vec<RowState> {
self.requested_entries()
.iter()
.map(AdapterInfo::classify)
.collect()
}
pub fn validated(&self) -> Option<Vec<AdapterInfo>> {
let entries = self.requested_entries();
let states = self.row_states();
if states.contains(&RowState::Garbled) {
return None;
}
if states.first() != Some(&RowState::Populated) {
return None;
}
let populated: Vec<AdapterInfo> = entries
.iter()
.zip(&states)
.filter(|(_, state)| **state == RowState::Populated)
.map(|(entry, _)| *entry)
.collect();
if populated.is_empty() {
return None;
}
let mut indices: Vec<c_int> = populated.iter().map(|row| row.i_adapter_index).collect();
indices.sort_unstable();
indices.dedup();
if indices.len() != populated.len() {
return None;
}
Some(populated)
}
}
#[cfg(target_os = "windows")]
pub type Adl2MainControlCreate = unsafe extern "C" fn(
callback: AdlMainMallocCallback,
enum_connected_adapters: c_int,
context: *mut AdlContextHandle,
) -> c_int;
#[cfg(target_os = "windows")]
#[allow(dead_code)]
pub type Adl2MainControlDestroy = unsafe extern "C" fn(context: AdlContextHandle) -> c_int;
#[cfg(target_os = "windows")]
pub type Adl2AdapterNumberOfAdaptersGet =
unsafe extern "C" fn(context: AdlContextHandle, num_adapters: *mut c_int) -> c_int;
#[cfg(target_os = "windows")]
pub type Adl2OverdriveCaps = unsafe extern "C" fn(
context: AdlContextHandle,
adapter_index: c_int,
supported: *mut c_int,
enabled: *mut c_int,
version: *mut c_int,
) -> c_int;
#[cfg(target_os = "windows")]
pub type Adl2NewQueryPmLogDataGet = unsafe extern "C" fn(
context: AdlContextHandle,
adapter_index: c_int,
output: *mut AdlPmLogDataOutput,
) -> c_int;
#[cfg(target_os = "windows")]
pub type Adl2AdapterAdapterInfoGet = unsafe extern "C" fn(
context: AdlContextHandle,
info: *mut AdapterInfo,
input_size: c_int,
) -> c_int;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_sensor_table_is_addressable_by_index() {
let mut output = AdlPmLogDataOutput::default();
output.sensors[27].supported = 1;
output.sensors[27].value = 71;
let base = &output.sensors[0] as *const AdlSingleSensorData as usize;
let entry = &output.sensors[27] as *const AdlSingleSensorData as usize;
assert_eq!(entry - base, 27 * 8);
let struct_base = &output as *const AdlPmLogDataOutput as usize;
assert_eq!(base - struct_base, 4);
}
#[test]
fn the_padded_buffer_gives_an_oversized_write_somewhere_to_land() {
assert!(
std::mem::size_of::<AdlPmLogDataBuffer>()
>= std::mem::size_of::<c_int>()
+ 4 * std::mem::size_of::<AdlSingleSensorData>() * ADL_PMLOG_MAX_SENSORS
);
let buffer = AdlPmLogDataBuffer::default();
let buffer_base = &buffer as *const AdlPmLogDataBuffer as usize;
let output_base = &buffer.output as *const AdlPmLogDataOutput as usize;
assert_eq!(buffer_base, output_base);
}
#[test]
fn validation_accepts_a_matching_or_absent_size_and_rejects_a_conflicting_one() {
let mut buffer = AdlPmLogDataBuffer::default();
buffer.output.ul_size = 0;
assert!(buffer.validated().is_some());
buffer.output.ul_size = std::mem::size_of::<AdlPmLogDataOutput>() as i32;
assert!(buffer.validated().is_some());
buffer.output.ul_size = 4096;
assert!(buffer.validated().is_none());
}
#[test]
fn default_reports_every_sensor_unsupported() {
let output = AdlPmLogDataOutput::default();
assert!(output.sensors.iter().all(|s| s.supported == 0));
assert_eq!(output.sensors.len(), ADL_PMLOG_MAX_SENSORS);
}
fn fill(field: &mut [u8; ADL_MAX_PATH], text: &str) {
field.fill(0);
field[..text.len()].copy_from_slice(text.as_bytes());
}
fn sane_entry(index: i32, pnp: &str) -> AdapterInfo {
let mut entry = AdapterInfo {
i_adapter_index: index,
i_bus_number: 3,
i_device_number: 0,
i_function_number: 0,
i_vendor_id: 1002,
i_present: 1,
i_exist: 1,
..AdapterInfo::default()
};
fill(
&mut entry.str_udid,
"PCI_VEN_1002&DEV_744C&REV_C8_6&12A2C3D4&0&19A",
);
fill(&mut entry.str_adapter_name, "AMD Radeon RX 7900 XTX");
fill(&mut entry.str_display_name, r"\\.\DISPLAY1");
fill(&mut entry.str_pnp_string, pnp);
entry
}
#[test]
fn the_four_string_fields_sit_at_their_transcribed_offsets() {
let info = AdapterInfo::default();
let base = &info as *const AdapterInfo as usize;
for (name, offset, expected) in [
("strUDID", info.str_udid.as_ptr() as usize - base, 8),
(
"strAdapterName",
info.str_adapter_name.as_ptr() as usize - base,
280,
),
(
"strDisplayName",
info.str_display_name.as_ptr() as usize - base,
536,
),
(
"strPNPString",
info.str_pnp_string.as_ptr() as usize - base,
1312,
),
] {
assert_eq!(offset, expected, "{name} is misplaced");
}
}
#[test]
fn adl_strings_accept_device_paths_and_reject_binary_garbage() {
let mut field = [0u8; ADL_MAX_PATH];
assert_eq!(adl_string(&field), Some(""));
fill(&mut field, r"PCI\VEN_1002&DEV_744C");
assert_eq!(adl_string(&field), Some(r"PCI\VEN_1002&DEV_744C"));
let unterminated = [b'A'; ADL_MAX_PATH];
assert_eq!(adl_string(&unterminated), None);
let mut binary = [0u8; ADL_MAX_PATH];
binary[0] = 0x01;
binary[1] = 0x9F;
assert_eq!(adl_string(&binary), None);
assert_eq!(adl_string_lossy(&field), r"PCI\VEN_1002&DEV_744C");
assert_eq!(adl_string_lossy(&unterminated).len(), ADL_MAX_PATH);
}
#[test]
fn a_realistic_entry_passes_and_layout_garbage_fails() {
assert!(sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19").looks_sane());
let mut entry = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
entry.i_size = std::mem::size_of::<AdapterInfo>() as c_int;
assert!(entry.looks_sane());
entry.i_size = 1568; assert!(!entry.looks_sane());
let mut entry = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
entry.i_adapter_index = i32::from_le_bytes(*b"PCI\\");
assert!(!entry.looks_sane());
let mut entry = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
entry.str_pnp_string = [b'x'; ADL_MAX_PATH];
assert!(!entry.looks_sane());
}
#[test]
fn the_adapter_array_reports_the_requested_size_but_allocates_more() {
let mut array = AdapterInfoArray::for_count(3);
assert_eq!(
array.input_size() as usize,
3 * std::mem::size_of::<AdapterInfo>()
);
assert_eq!(array.requested_entries().len(), 3);
assert!(array.entries.len() >= 2 * array.requested);
let base = array.as_mut_ptr() as usize;
assert_eq!(base, array.entries.as_ptr() as usize);
}
#[test]
fn an_inexpressible_request_size_advertises_zero_rather_than_wrapping() {
let array = AdapterInfoArray {
entries: Vec::new(),
requested: usize::MAX / 8,
};
assert_eq!(array.input_size(), 0);
}
#[test]
fn rows_classify_by_what_the_driver_did_to_them() {
assert_eq!(AdapterInfo::poisoned().classify(), RowState::Untouched);
let blank = AdapterInfo::default();
assert!(blank.is_blank());
assert!(blank.looks_sane());
assert_eq!(blank.classify(), RowState::Blank);
let written = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
assert_eq!(written.classify(), RowState::Populated);
let mut garbage = written;
garbage.str_pnp_string = [0xFF; ADL_MAX_PATH];
assert_eq!(garbage.classify(), RowState::Garbled);
}
#[test]
fn a_trailing_untouched_run_is_accepted_and_dropped() {
let mut array = AdapterInfoArray::for_count(3);
array.entries[0] = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
array.entries[1] = sane_entry(1, r"PCI\VEN_1002&DEV_164E\4&FEDC&0&41");
let accepted = array.validated().expect("trailing untouched run must pass");
assert_eq!(accepted.len(), 2);
assert_eq!(
array.row_states(),
vec![
RowState::Populated,
RowState::Populated,
RowState::Untouched
]
);
assert_eq!(array.requested_entries().len(), 3);
}
#[test]
fn blank_rows_are_dropped_whether_trailing_or_interleaved() {
let mut array = AdapterInfoArray::for_count(3);
array.entries[0] = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
array.entries[1] = AdapterInfo::default();
array.entries[2] = sane_entry(2, r"PCI\VEN_1002&DEV_164E\4&FEDC&0&41");
let accepted = array.validated().expect("interleaved blanks must pass");
assert_eq!(accepted.len(), 2);
assert_eq!(accepted[0].i_adapter_index, 0);
assert_eq!(accepted[1].i_adapter_index, 2);
let mut array = AdapterInfoArray::for_count(2);
array.entries[0] = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
array.entries[1] = AdapterInfo::default();
assert_eq!(array.validated().map(|rows| rows.len()), Some(1));
}
#[test]
fn a_table_without_a_populated_first_row_is_rejected() {
let mut array = AdapterInfoArray::for_count(2);
array.entries[0] = AdapterInfo::default();
array.entries[1] = sane_entry(1, r"PCI\VEN_1002&DEV_164E\4&FEDC&0&41");
assert!(array.validated().is_none());
let untouched = AdapterInfoArray::for_count(2);
assert_eq!(
untouched.row_states(),
vec![RowState::Untouched, RowState::Untouched]
);
assert!(untouched.validated().is_none());
}
#[test]
fn duplicate_adapter_indices_among_populated_rows_are_rejected() {
let mut array = AdapterInfoArray::for_count(2);
array.entries[0] = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
array.entries[1] = sane_entry(0, r"PCI\VEN_1002&DEV_164E\4&FEDC&0&41");
assert!(array.validated().is_none());
}
#[test]
fn validation_rejects_the_whole_table_when_any_row_is_garbage() {
let mut array = AdapterInfoArray::for_count(2);
array.entries[0] = sane_entry(0, r"PCI\VEN_1002&DEV_744C\6&ABCD&0&19");
array.entries[1] = sane_entry(1, r"PCI\VEN_1002&DEV_164E\4&FEDC&0&41");
assert!(array.validated().is_some());
assert_eq!(array.validated().map(|rows| rows.len()), Some(2));
array.entries[1].str_udid = [0xFF; ADL_MAX_PATH];
assert!(array.validated().is_none());
}
}