df_displmgr 0.1.0

A unified cross-platform display configuration manager for Windows (CCD) and Linux (wlroots/DRM).
// backends/linux/dispmgr_drm.rs
//! # DRM/KMS Backend Implementation
//! Implements display management traits using Linux Direct Rendering Manager.
//! Features Atomic KMS for flicker-free, synchronized updates and dynamic property discovery.

use async_trait::async_trait;
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use drm::control::{AtomicCommitFlags, atomic::AtomicModeReq};

use crate::error::{DisplayError, DisplayResult};
use crate::traits::{OutputEditable, UniversalTopology};
use crate::types::{OutputState, DisplayRotation, HdrState, HdrMode, DisplayId, Extent2D, Point2D, Rect};

pub mod displmgr_drm_sys;
pub mod displmgr_drm_api;

use self::displmgr_drm_sys::{DrmResourceIds, DrmPropertyCache, rotation_to_drm_value};
use self::displmgr_drm_api::probe_drm_hardware;

/// Linux Direct Rendering Manager (DRM) Backend using Atomic KMS.
pub struct DrmTopology {
    pub device: File,
    pub outputs: Vec<OutputState>,
    pub atomic_req: AtomicModeReq,
    pub resource_map: HashMap<DisplayId, DrmResourceIds>,
    pub property_cache: DrmPropertyCache,
    pub dirty: bool,
}

impl DrmTopology {
    /// Internal helper to locate queryable property tokens across active connectors.
    pub fn find_prop_id(&self, conn: drm::control::connector::Handle, name: &str) -> Option<drm::control::property::Handle> {
        self.property_cache.props.get(&conn)?.get(name).cloned()
    }

    /// Wake inactive outputs and return their `DisplayId`s so they can be
    /// restored later. If `keep_active` is provided, that output will remain
    /// enabled after restore.
    pub fn snapshot_wake_inactive_outputs(&mut self, keep_active: Option<&DisplayId>) -> Vec<DisplayId> {
        let mut activated = Vec::new();
        for out in &mut self.outputs {
            if !out.enabled {
                if let Some(k) = keep_active {
                    if &out.identity.id == k { continue; }
                }
                out.enabled = true;
                activated.push(out.identity.id.clone());
            }
        }
        activated
    }

    /// Restore outputs that were previously activated by `snapshot_wake_inactive_outputs`.
    pub fn restore_inactive_outputs(&mut self, inactive_ids: &[DisplayId]) {
        for id in inactive_ids {
            if let Some(out) = self.outputs.iter_mut().find(|o| &o.identity.id == id) {
                out.enabled = false;
            }
        }
    }
}

// ---------------------------------------------------------------------------
// Implementation of the transient output modifier (OutputEditable)
// ---------------------------------------------------------------------------
pub struct DrmOutputEditor<'a> {
    pub topology: &'a mut DrmTopology,
    pub target_id: DisplayId,
}

macro_rules! find_drm_output {
    ($self:expr) => {
        {
            let target_id = $self.target_id.clone();
            $self.topology.outputs.iter_mut().find(|o| o.identity.id == target_id)
                .ok_or_else(|| DisplayError::NotFound(target_id))
        }
    };
}

impl<'a> OutputEditable for DrmOutputEditor<'a> {
    fn set_rotation(&mut self, rotation: DisplayRotation) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if out.rotation != rotation {
            out.rotation = rotation;
            
            let res = self.topology.resource_map.get(&self.target_id)
                .ok_or_else(|| DisplayError::NotFound(self.target_id.clone()))?;
            
            if let Some(prop_rot) = self.topology.find_prop_id(res.connector_id, "rotation") {
                let val = rotation_to_drm_value(rotation);
                self.topology.atomic_req.add_property(res.primary_plane_id, prop_rot, val);
                self.topology.dirty = true;
            }
        }
        Ok(self)
    }

    fn set_resolution(&mut self, extent: Extent2D) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if out.geometry.size != extent {
            out.geometry.size = extent;
            self.topology.dirty = true;
            // Native mode resolution swaps require dynamic generation of formal DRM property blobs.
            // This is handled atomically inside the transactional commit frame execution layer.
        }
        Ok(self)
    }

    fn set_position(&mut self, position: Point2D) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if out.geometry.origin != position {
            out.geometry.origin = position;
            
            let res = self.topology.resource_map.get(&self.target_id)
                .ok_or_else(|| DisplayError::NotFound(self.target_id.clone()))?;
            
            if let Some(prop_x) = self.topology.find_prop_id(res.connector_id, "CRTC_X") {
                self.topology.atomic_req.add_property(res.primary_plane_id, prop_x, position.x as u64);
            }
            if let Some(prop_y) = self.topology.find_prop_id(res.connector_id, "CRTC_Y") {
                self.topology.atomic_req.add_property(res.primary_plane_id, prop_y, position.y as u64);
            }
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_refresh_rate(&mut self, rate: u32) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if out.refresh_rate != rate {
            out.refresh_rate = rate;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_primary(&mut self) -> DisplayResult<&mut dyn OutputEditable> {
        for out in &mut self.topology.outputs {
            out.is_primary = false;
        }
        let out = find_drm_output!(self)?;
        out.is_primary = true;
        self.topology.dirty = true;
        Ok(self)
    }

    fn set_hdr(&mut self, state: HdrState, mode: HdrMode) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if out.hdr_state != state || out.hdr_mode != mode {
            out.hdr_state = state;
            out.hdr_mode = mode;
            
            let res = self.topology.resource_map.get(&self.target_id)
                .ok_or_else(|| DisplayError::NotFound(self.target_id.clone()))?;
            
            if let Some(prop_hdr) = self.topology.find_prop_id(res.connector_id, "HDR_OUTPUT_METADATA") {
                let val = if state == HdrState::Enabled { 1 } else { 0 };
                self.topology.atomic_req.add_property(res.connector_id, prop_hdr, val);
                self.topology.dirty = true;
            }
        }
        Ok(self)
    }

    fn set_scale(&mut self, scale: f64) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if (out.scale - scale).abs() > f64::EPSILON {
            out.scale = scale;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_enabled(&mut self, enabled: bool) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_drm_output!(self)?;
        if out.enabled != enabled {
            out.enabled = enabled;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn clone_from(&mut self, source_id: &DisplayId) -> DisplayResult<&mut dyn OutputEditable> {
        let source_state = self.topology.outputs.iter()
            .find(|o| o.identity.id == *source_id)
            .cloned()
            .ok_or_else(|| DisplayError::NotFound(source_id.clone()))?;

        let dest = find_drm_output!(self)?;
        dest.geometry = source_state.geometry;
        dest.refresh_rate = source_state.refresh_rate;
        dest.rotation = source_state.rotation;
        dest.scale = source_state.scale;
        
        self.topology.dirty = true;
        Ok(self)
    }

    fn get_state(&self) -> OutputState {
        self.topology.outputs.iter()
            .find(|o| o.identity.id == self.target_id)
            .cloned()
            .unwrap_or_default()
    }
}

// ---------------------------------------------------------------------------
// Global Interface Mapping (UniversalTopology Trait Compliance)
// ---------------------------------------------------------------------------
#[async_trait]
impl UniversalTopology for DrmTopology {
    fn acquire() -> DisplayResult<Self> {
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/dri/card0")
            .map_err(|e| DisplayError::Io(e))?;

        let (outputs, resource_map, property_cache) = probe_drm_hardware(&file)?;

        Ok(Self {
            device: file,
            outputs,
            atomic_req: AtomicModeReq::new(),
            resource_map,
            property_cache,
            dirty: false,
        })
    }

    fn get_outputs(&self) -> Vec<OutputState> {
        self.outputs.clone()
    }

    fn edit_output(&mut self, id: &DisplayId) -> DisplayResult<Box<dyn OutputEditable + '_>> {
        if !self.outputs.iter().any(|o| o.identity.id == *id) {
            return Err(DisplayError::NotFound(id.clone()));
        }
        Ok(Box::new(DrmOutputEditor {
            topology: self,
            target_id: id.clone(),
        }))
    }

    fn set_persistence(&mut self, _enabled: bool) -> &mut Self {
        // DRM native interface configuration state changes apply directly via low level kernel frame flushes.
        // True persistence on Linux bare-metal setups is achieved via systemd/udev storage architectures.
        self
    }

    async fn validate(&self) -> DisplayResult<()> {
        let flags = AtomicCommitFlags::TEST_ONLY;
        self.device.atomic_commit(flags, self.atomic_req.clone())
            .map_err(|_| DisplayError::ConfigurationRejected)?;
        Ok(())
    }

    async fn commit(&mut self) -> DisplayResult<()> {
        if !self.dirty {
            return Ok(());
        }

        let device = self.device.try_clone().map_err(|e| DisplayError::Io(e))?;
        let req = self.atomic_req.clone();
        
        tokio::task::spawn_blocking(move || -> DisplayResult<()> {
            let flags = AtomicCommitFlags::ALLOW_MODESET;
            device.atomic_commit(flags, req)
                .map_err(|e| DisplayError::BackendError(format!("KMS Atomic Flush Failure: {}", e)))?;
            Ok(())
        })
        .await
        .map_err(|e| DisplayError::BackendError(format!("DRM thread pool panic: {}", e)))??;

        // Reset tracking structures following a successful atomic hardware flush pass
        self.atomic_req = AtomicModeReq::new();
        self.dirty = false;
        Ok(())
    }
}