refactor: put intel drm logic behind a trait, add mock

This commit is contained in:
Ilya Zlobintsev committed 2026-08-29 12:21:15 +03:00
1 parent 40dcf29841
commit 2b24043d2b
14 files changed
+403 -157

No files matched your search

Generated
+1
View File
@@ -1813,6 +1813,7 @@ dependencies = [
"indexmap",
"insta",
"jiff",
"lact-daemon",
"lact-schema",
"libcopes",
"libdisplay-info",
+2
View File
@@ -8,6 +8,7 @@ default = ["nvidia", "display-info"]
bench = ["dep:divan"]
nvidia = []
display-info = ["dep:libdisplay-info"]
mock = []
[dependencies]
lact-schema = { path = "../lact-schema" }
@@ -57,6 +58,7 @@ pretty_assertions = { workspace = true }
insta = { version = "1.48.0", features = ["json", "yaml"] }
easy_fuser = { version = "0.5.0", default-features = false, features = ["parallel"] }
tempfile = "3.27.0"
lact-daemon = { path = ".", features= ["mock"] }
[build-dependencies]
bindgen = "0.72"
+9
View File
@@ -29,6 +29,7 @@ use lact_schema::{
ClocksInfo, DeviceInfo, DeviceStats, GpuPciInfo, PciInfo, PowerStates, config::GpuConfig,
};
use std::io;
use std::os::fd::OwnedFd;
use std::sync::LazyLock;
use std::sync::Mutex;
use std::{collections::HashMap, fs, path::PathBuf, rc::Rc};
@@ -149,6 +150,14 @@ impl CommonControllerInfo {
))
}
pub fn open_drm_render(&self) -> io::Result<OwnedFd> {
fs::OpenOptions::new()
.read(true)
.write(true)
.open(self.get_drm_render()?)
.map(OwnedFd::from)
}
pub fn get_drm_card(&self) -> io::Result<PathBuf> {
fs::canonicalize(format!("/dev/dri/by-path/pci-{}-card", self.pci_slot_name))
}
+30 -133
View File
@@ -1,11 +1,13 @@
#[cfg_attr(test, allow(dead_code))]
mod drm;
use self::drm::DrmProvider;
#[cfg(feature = "mock")]
use self::drm::mock::MockDrmProvider;
use self::drm::{i915::I915DrmProvider, xe::XeDrmProvider};
use super::{CommonControllerInfo, GpuController};
use crate::{
bindings::intel::{
IntelDrm, drm_i915_gem_memory_class_I915_MEMORY_CLASS_DEVICE,
drm_xe_memory_class_DRM_XE_MEM_REGION_CLASS_VRAM,
},
bindings::intel::IntelDrm,
server::gpu_controller::common::fdinfo::{self, DrmUtilMap},
};
use amdgpu_sysfs::{gpu_handle::power_profile_mode::PowerProfileModesTable, hw_mon::Temperature};
@@ -15,9 +17,9 @@ use indexmap::IndexMap;
use lact_schema::FanControlMode::Static;
use lact_schema::{
ClocksInfo, ClocksTable, ClockspeedStats, DeviceApiInfo, DeviceFlag, DeviceInfo, DeviceStats,
DeviceType, DrmInfo, DrmMemoryInfo, FanControlMode, FanStats, IntelClocksTable, IntelDrmInfo,
LinkInfo, PowerState, PowerStates, PowerStats, ProcessList, ProcessUtilizationType,
TemperatureEntry, VoltageStats, VramStats, config::GpuConfig,
DeviceType, DrmInfo, FanControlMode, FanStats, IntelClocksTable, LinkInfo, PowerState,
PowerStates, PowerStats, ProcessList, ProcessUtilizationType, TemperatureEntry, VoltageStats,
VramStats, config::GpuConfig,
};
use lact_schema::{FanCurveMap, config::FanCurve};
use std::{
@@ -27,7 +29,6 @@ use std::{
fmt::{self, Display},
fs,
io::{BufRead, BufReader, ErrorKind},
os::{fd::AsRawFd, raw::c_int},
path::{Path, PathBuf},
str::FromStr,
time::Instant,
@@ -61,8 +62,7 @@ pub struct IntelGpuController {
common: CommonControllerInfo,
tile_gts: Vec<PathBuf>,
hwmon_path: Option<PathBuf>,
drm_file: fs::File,
drm: &'static IntelDrm,
drm: Box<dyn DrmProvider>,
last_drm_util: RefCell<Option<DrmUtilMap>>,
last_gpu_busy: Cell<Option<(Instant, u64)>>,
#[allow(dead_code)]
@@ -112,15 +112,22 @@ impl IntelGpuController {
);
tile_gts.sort();
}
let drm_file = if cfg!(not(test)) {
let drm_path = common.get_drm_render()?;
fs::OpenOptions::new()
.read(true)
.write(true)
.open(drm_path)
.context("Could not open DRM file")?
} else {
fs::File::open("/dev/null").unwrap()
#[allow(unused_labels)]
let drm: Box<dyn DrmProvider> = 'drm: {
#[cfg(feature = "mock")]
if let Some(mock) = MockDrmProvider::new(&common.sysfs_path) {
break 'drm Box::new(mock) as Box<dyn DrmProvider>;
}
let drm_fd = common
.open_drm_render()
.context("Could not open DRM file")?;
match driver_type {
DriverType::I915 => Box::new(I915DrmProvider::new(drm, drm_fd)),
DriverType::Xe => Box::new(XeDrmProvider::new(drm_fd)),
}
};
let hwmon_path = fs::read_dir(common.sysfs_path.join("hwmon"))
@@ -135,7 +142,6 @@ impl IntelGpuController {
driver_type,
tile_gts,
hwmon_path,
drm_file,
drm,
last_drm_util: RefCell::new(None),
last_gpu_busy: Cell::new(None),
@@ -272,33 +278,6 @@ impl IntelGpuController {
self.write_file(path, contents)
}
fn get_drm_info_i915(&self) -> IntelDrmInfo {
IntelDrmInfo {
execution_units: self.drm_try(IntelDrm::drm_intel_get_eu_total),
subslices: self.drm_try(IntelDrm::drm_intel_get_subslice_total),
}
}
#[allow(clippy::unused_self)]
fn get_drm_info_xe(&self) -> IntelDrmInfo {
IntelDrmInfo {
execution_units: None,
subslices: None,
}
}
#[cfg_attr(test, allow(unreachable_code, unused_variables))]
fn drm_try<T: Default>(&self, f: unsafe fn(&IntelDrm, c_int, *mut T) -> c_int) -> Option<T> {
#[cfg(test)]
return None;
unsafe {
let mut out = T::default();
let result = f(self.drm, self.drm_file.as_raw_fd(), &raw mut out);
if result == 0 { Some(out) } else { None }
}
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
@@ -487,77 +466,6 @@ impl IntelGpuController {
Some(reasons)
}
fn get_vram_info(&self) -> IntelVramInfo {
let mut total = 0;
let mut used = 0;
let mut cpu_accessible_total = 0;
let mut cpu_accessible_used = 0;
match self.driver_type {
DriverType::I915 => {
if let Ok(Some(query)) = drm::i915::query_memory_regions(&self.drm_file) {
let mut i915_unallocated = 0;
let mut cpu_unallocated = 0;
unsafe {
let regions = query.regions.as_slice(query.num_regions as usize);
for region_info in regions {
if u32::from(region_info.region.memory_class)
== drm_i915_gem_memory_class_I915_MEMORY_CLASS_DEVICE
{
total += region_info.probed_size;
i915_unallocated += region_info.unallocated_size;
let cpu_region_info = region_info.__bindgen_anon_1.__bindgen_anon_1;
if cpu_region_info.probed_cpu_visible_size > 0 {
cpu_accessible_total += cpu_region_info.probed_cpu_visible_size;
cpu_unallocated += cpu_region_info.unallocated_cpu_visible_size;
}
}
}
}
if total > 0 {
used = total - i915_unallocated;
}
if cpu_accessible_total > 0 {
cpu_accessible_used = cpu_accessible_total - cpu_unallocated;
}
}
}
DriverType::Xe => {
if let Ok(Some(query)) = drm::xe::query_mem_regions(&self.drm_file) {
unsafe {
let regions = query.mem_regions.as_slice(query.num_mem_regions as usize);
for region_info in regions {
if u32::from(region_info.mem_class)
== drm_xe_memory_class_DRM_XE_MEM_REGION_CLASS_VRAM
{
total += region_info.total_size;
used += region_info.used;
if region_info.cpu_visible_size > 0 {
cpu_accessible_total += region_info.cpu_visible_size;
}
}
}
}
}
}
}
IntelVramInfo {
total,
used,
mem_info: DrmMemoryInfo {
cpu_accessible_used,
cpu_accessible_total,
resizeable_bar: Some(cpu_accessible_total == total),
},
}
}
#[allow(clippy::cast_precision_loss)]
fn get_power_cap(&self) -> Option<f64> {
self.read_hwmon_file(POWER_CAP_NAMES, true)
@@ -806,7 +714,7 @@ impl GpuController for IntelGpuController {
}
fn device_type(&self) -> DeviceType {
if self.get_vram_info().total > 0 {
if self.drm.get_vram_info().total > 0 {
DeviceType::Dedicated
} else {
DeviceType::Integrated
@@ -829,15 +737,10 @@ impl GpuController for IntelGpuController {
DeviceApiInfo::default()
};
let vram_info = self.get_vram_info();
let drm_info = DrmInfo {
intel: match self.driver_type {
DriverType::I915 => self.get_drm_info_i915(),
DriverType::Xe => self.get_drm_info_xe(),
},
intel: self.drm.get_intel_info(),
vram_clock_ratio: 1.0,
memory_info: Some(vram_info.mem_info),
memory_info: Some(self.drm.get_vram_info().mem_info),
..Default::default()
};
@@ -972,7 +875,7 @@ impl GpuController for IntelGpuController {
..Default::default()
};
let vram_info = self.get_vram_info();
let vram_info = self.drm.get_vram_info();
let vram = VramStats {
total: match vram_info.total {
0 => None,
@@ -1137,12 +1040,6 @@ impl fmt::Display for FrequencyType {
}
}
struct IntelVramInfo {
total: u64,
used: u64,
mem_info: DrmMemoryInfo,
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
@@ -1,6 +1,9 @@
pub mod i915;
#[cfg(feature = "mock")]
pub mod mock;
pub mod xe;
use lact_schema::{DrmMemoryInfo, IntelDrmInfo};
use std::{alloc, ops::Deref};
pub struct DrmBox<T> {
@@ -23,3 +26,15 @@ impl<T> Drop for DrmBox<T> {
}
}
}
pub trait DrmProvider {
fn get_intel_info(&self) -> IntelDrmInfo;
fn get_vram_info(&self) -> VramInfo;
}
pub struct VramInfo {
pub total: u64,
pub used: u64,
pub mem_info: DrmMemoryInfo,
}
@@ -3,17 +3,104 @@
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use super::DrmBox;
use crate::bindings::intel::drm_i915_query_item;
use super::{DrmBox, DrmProvider, VramInfo};
use crate::bindings::intel::{
DRM_COMMAND_BASE, DRM_I915_QUERY_MEMORY_REGIONS, DRM_IOCTL_BASE, drm_i915_query,
DRM_COMMAND_BASE, DRM_I915_QUERY_MEMORY_REGIONS, DRM_IOCTL_BASE,
drm_i915_gem_memory_class_I915_MEMORY_CLASS_DEVICE, drm_i915_query,
drm_i915_query_memory_regions,
};
use crate::bindings::intel::{IntelDrm, drm_i915_query_item};
use lact_schema::{DrmMemoryInfo, IntelDrmInfo};
use nix::{errno::Errno, ioctl_readwrite};
use std::{alloc, fs::File, mem, os::fd::AsRawFd, ptr};
use std::ffi::c_int;
use std::os::fd::OwnedFd;
use std::{alloc, mem, os::fd::AsRawFd, ptr};
const DRM_I915_QUERY: u32 = 0x39;
pub struct I915DrmProvider {
drm: &'static IntelDrm,
fd: OwnedFd,
}
impl I915DrmProvider {
pub fn new(drm: &'static IntelDrm, fd: OwnedFd) -> Self {
Self { drm, fd }
}
fn drm_try<T: Default>(&self, f: unsafe fn(&IntelDrm, c_int, *mut T) -> c_int) -> Option<T> {
unsafe {
let mut out = T::default();
let result = f(self.drm, self.fd.as_raw_fd(), &raw mut out);
if result == 0 { Some(out) } else { None }
}
}
}
impl DrmProvider for I915DrmProvider {
fn get_intel_info(&self) -> IntelDrmInfo {
IntelDrmInfo {
execution_units: self.drm_try(IntelDrm::drm_intel_get_eu_total),
subslices: self.drm_try(IntelDrm::drm_intel_get_subslice_total),
}
}
fn get_vram_info(&self) -> VramInfo {
let mut total = 0;
let mut used = 0;
let mut cpu_accessible_total = 0;
let mut cpu_accessible_used = 0;
let result = unsafe {
query_item::<drm_i915_query_memory_regions>(
self.fd.as_raw_fd(),
DRM_I915_QUERY_MEMORY_REGIONS,
)
};
if let Ok(Some(query)) = result {
let mut i915_unallocated = 0;
let mut cpu_unallocated = 0;
unsafe {
let regions = query.regions.as_slice(query.num_regions as usize);
for region_info in regions {
if u32::from(region_info.region.memory_class)
== drm_i915_gem_memory_class_I915_MEMORY_CLASS_DEVICE
{
total += region_info.probed_size;
i915_unallocated += region_info.unallocated_size;
let cpu_region_info = region_info.__bindgen_anon_1.__bindgen_anon_1;
if cpu_region_info.probed_cpu_visible_size > 0 {
cpu_accessible_total += cpu_region_info.probed_cpu_visible_size;
cpu_unallocated += cpu_region_info.unallocated_cpu_visible_size;
}
}
}
}
if total > 0 {
used = total - i915_unallocated;
}
if cpu_accessible_total > 0 {
cpu_accessible_used = cpu_accessible_total - cpu_unallocated;
}
}
VramInfo {
total,
used,
mem_info: DrmMemoryInfo {
cpu_accessible_used,
cpu_accessible_total,
resizeable_bar: Some(cpu_accessible_total == total),
},
}
}
}
ioctl_readwrite!(
i915_query,
DRM_IOCTL_BASE,
@@ -55,9 +142,3 @@ unsafe fn query_item<T>(fd: i32, query_id: u32) -> Result<Option<DrmBox<T>>, Err
Ok(Some(DrmBox { data, layout }))
}
}
pub fn query_memory_regions(
fd: &File,
) -> Result<Option<DrmBox<drm_i915_query_memory_regions>>, Errno> {
unsafe { query_item(fd.as_raw_fd(), DRM_I915_QUERY_MEMORY_REGIONS) }
}
@@ -0,0 +1,38 @@
use super::{DrmProvider, VramInfo};
use lact_schema::{DeviceStats, DrmInfo, IntelDrmInfo};
use serde::de::DeserializeOwned;
use std::path::Path;
pub struct MockDrmProvider {
info: DrmInfo,
stats: DeviceStats,
}
impl MockDrmProvider {
pub fn new(sysfs: &Path) -> Option<Self> {
Some(Self {
info: read_json(sysfs, "drm_info.json")?,
stats: read_json(sysfs, "stats.json")?,
})
}
}
fn read_json<T: DeserializeOwned>(sysfs: &Path, name: &str) -> Option<T> {
std::fs::read_to_string(sysfs.join(name))
.ok()
.and_then(|contents| serde_json::from_str::<T>(&contents).ok())
}
impl DrmProvider for MockDrmProvider {
fn get_intel_info(&self) -> IntelDrmInfo {
self.info.intel.clone()
}
fn get_vram_info(&self) -> VramInfo {
VramInfo {
total: self.stats.vram.total.unwrap_or(0),
used: self.stats.vram.used.unwrap_or(0),
mem_info: self.info.memory_info.clone().unwrap_or_default(),
}
}
}
@@ -1,11 +1,74 @@
use super::{DrmBox, DrmProvider, VramInfo};
use crate::bindings::intel::{
DRM_COMMAND_BASE, DRM_IOCTL_BASE, DRM_XE_DEVICE_QUERY, DRM_XE_DEVICE_QUERY_MEM_REGIONS,
drm_xe_device_query, drm_xe_query_mem_regions,
drm_xe_device_query, drm_xe_memory_class_DRM_XE_MEM_REGION_CLASS_VRAM,
drm_xe_query_mem_regions,
};
use lact_schema::{DrmMemoryInfo, IntelDrmInfo};
use nix::{errno::Errno, ioctl_readwrite};
use std::{alloc, fs::File, mem, os::fd::AsRawFd};
use std::{
alloc, mem,
os::fd::{AsRawFd, OwnedFd},
};
use super::DrmBox;
pub struct XeDrmProvider {
fd: OwnedFd,
}
impl XeDrmProvider {
pub fn new(fd: OwnedFd) -> Self {
Self { fd }
}
}
impl DrmProvider for XeDrmProvider {
fn get_intel_info(&self) -> IntelDrmInfo {
IntelDrmInfo::default()
}
fn get_vram_info(&self) -> VramInfo {
let mut total = 0;
let mut used = 0;
let mut cpu_accessible_total = 0;
let mut cpu_accessible_used = 0;
let result = unsafe {
query_item::<drm_xe_query_mem_regions>(
self.fd.as_raw_fd(),
DRM_XE_DEVICE_QUERY_MEM_REGIONS,
)
};
if let Ok(Some(query)) = result {
unsafe {
let regions = query.mem_regions.as_slice(query.num_mem_regions as usize);
for region_info in regions {
if u32::from(region_info.mem_class)
== drm_xe_memory_class_DRM_XE_MEM_REGION_CLASS_VRAM
{
total += region_info.total_size;
used += region_info.used;
if region_info.cpu_visible_size > 0 {
cpu_accessible_total += region_info.cpu_visible_size;
cpu_accessible_used += region_info.cpu_visible_used;
}
}
}
}
}
VramInfo {
total,
used,
mem_info: DrmMemoryInfo {
cpu_accessible_used,
cpu_accessible_total,
resizeable_bar: Some(cpu_accessible_total == total),
},
}
}
}
ioctl_readwrite!(
xe_device_query,
@@ -43,7 +106,3 @@ unsafe fn query_item<T>(fd: i32, query_id: u32) -> Result<Option<DrmBox<T>>, Err
Ok(Some(DrmBox { data, layout }))
}
}
pub fn query_mem_regions(fd: &File) -> Result<Option<DrmBox<drm_xe_query_mem_regions>>, Errno> {
unsafe { query_item(fd.as_raw_fd(), DRM_XE_DEVICE_QUERY_MEM_REGIONS) }
}
@@ -1509,15 +1509,12 @@ impl GpuController for NvidiaGpuController {
#[cfg(feature = "display-info")]
fn populate_displays_info(&self, info: &mut lact_schema::DisplaysInfo) -> anyhow::Result<()> {
use lact_schema::DisplayConnector;
use std::fs;
use std::os::fd::AsRawFd as _;
if let Some(handle) = &self.driver_handle {
let drm_path = self.common.get_drm_render()?;
let drm_file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(drm_path)
let drm_file = self
.common
.open_drm_render()
.context("Could not open DRM file")?;
for (key, display_info) in &mut info.displays {
@@ -0,0 +1,6 @@
---
source: lact-daemon/src/tests/mod.rs
assertion_line: 123
expression: write_commands
---
[]
@@ -0,0 +1,94 @@
---
source: lact-daemon/src/tests/mod.rs
assertion_line: 52
expression: device_info
---
{
"clocks_info": null,
"info": {
"driver": "i915",
"drm_info": {
"vram_clock_ratio": 1.0
},
"flags": [
"DumpableVBios",
"ConfigurableFanControl"
],
"link_info": {
"current_speed": "2.5 GT/s PCIe",
"current_width": "1"
},
"opencl_instances": [
{
"c_version": "OpenCL C 1.2 ",
"compute_units": 128,
"device_name": "Intel(R) Arc(TM) A380 Graphics",
"driver_version": "25.48.36300",
"global_memory": 6064541696,
"local_memory": 65536,
"platform_name": "Intel(R) OpenCL Graphics",
"version": "OpenCL 3.0 NEO ",
"workgroup_size": 1024
}
],
"pci_info": {
"device_pci_info": {
"model": "DG2 [Arc A380]",
"model_id": "56A5",
"vendor": "Intel Corporation",
"vendor_id": "8086"
},
"subsystem_pci_info": {
"model": "ASRock Challenger ITX Arc A380",
"model_id": "6004",
"vendor": "ASRock Incorporation",
"vendor_id": "1849"
}
},
"vulkan_instances": []
},
"pci_info": {
"device_pci_info": {
"model": "DG2 [Arc A380]",
"model_id": "56A5",
"vendor": "Intel Corporation",
"vendor_id": "8086"
},
"subsystem_pci_info": {
"model": "ASRock Challenger ITX Arc A380",
"model_id": "6004",
"vendor": "ASRock Incorporation",
"vendor_id": "1849"
}
},
"power_profile_modes": null,
"power_states": {
"core": [],
"vram": []
},
"stats": {
"clockspeed": {
"sensors": {}
},
"fan": {
"control_enabled": false,
"pmfw_info": {},
"speed_current": 0
},
"power": {},
"temps": {
"1": {
"crit": null,
"crit_hyst": null,
"current": 55.0,
"display_only": false,
"primary": true
}
},
"voltage": {
"gpu": 603,
"sensors": {}
},
"vram": {}
}
}
+1 -1
View File
@@ -464,7 +464,7 @@ pub struct IntelDrmInfo {
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DrmMemoryInfo {
pub cpu_accessible_used: u64,
pub cpu_accessible_total: u64,
@@ -0,0 +1,10 @@
{
"execution_units": 128,
"memory_info": {
"cpu_accessible_total": 6383730688,
"cpu_accessible_used": 22360064,
"resizeable_bar": true
},
"subslices": 8,
"vram_clock_ratio": 1.0
}
@@ -0,0 +1,37 @@
{
"clockspeed": {
"gpu_clockspeed": 600,
"sensors": {},
"target_gpu_clockspeed": 600
},
"fan": {
"control_enabled": false,
"pmfw_info": {},
"speed_current": 741
},
"power": {
"cap_current": 65.0,
"cap_default": 65.0,
"cap_max": 400.0,
"cap_min": 0.0,
"current": 18.0
},
"temps": {
"gpu": {
"crit": null,
"crit_hyst": null,
"current": 54.0,
"display_only": true,
"primary": true
}
},
"throttle_info": {},
"voltage": {
"gpu": 605,
"sensors": {}
},
"vram": {
"total": 6383730688,
"used": 22360064
}
}