yuno 0.2.0

Multimedia UI layout and rendering framework powered by Skia.
use crate::committer::Committer;
use crate::render::device::DrmCard;
use crate::render::framebuffer::Framebuffer;
use anyhow::{Context, Result, anyhow, bail};
use drm::Device as DrmDevice;
use drm::control::{
    self, AtomicCommitFlags, Device as ControlDevice, atomic::AtomicModeReq, connector, crtc,
    plane, property,
};
use std::rc::Rc;
use std::sync::mpsc::Receiver;
use std::sync::{Arc, mpsc};
use std::thread;
use std::thread::JoinHandle;

#[derive(Debug)]
struct AtomicProps {
    conn_crtc_id: property::Handle,

    crtc_mode_id: property::Handle,
    crtc_active: property::Handle,

    plane_fb_id: property::Handle,
    plane_crtc_id: property::Handle,
    plane_src_x: property::Handle,
    plane_src_y: property::Handle,
    plane_src_w: property::Handle,
    plane_src_h: property::Handle,
    plane_crtc_x: property::Handle,
    plane_crtc_y: property::Handle,
    plane_crtc_w: property::Handle,
    plane_crtc_h: property::Handle,
}

pub struct AtomicCommitter {
    pub d: Arc<DrmCard>,
    pub fb: Option<Rc<Framebuffer<DrmCard>>>,
    connector: connector::Info,
    mode: control::Mode,
    crtc: crtc::Info,
    plane: plane::Handle,
    props: AtomicProps,

    srx: Receiver<control::Event>,
    #[allow(unused)]
    event_handle: JoinHandle<Result<()>>,
}

impl AtomicCommitter {
    fn build_atomic_req(&self, fb: &Framebuffer<DrmCard>, mode_set: bool) -> Result<AtomicModeReq> {
        let (fb_w, fb_h) = (fb.width, fb.height);
        let (mode_w, mode_h) = self.mode.size();

        let scale_w = mode_w as f64 / fb_w as f64;
        let scale_h = mode_h as f64 / fb_h as f64;
        let scale = scale_w.min(scale_h);

        let crtc_w = (fb_w as f64 * scale).round() as u64;
        let crtc_h = (fb_h as f64 * scale).round() as u64;

        let crtc_x = ((mode_w as u64 - crtc_w) / 2) as i64;
        let crtc_y = ((mode_h as u64 - crtc_h) / 2) as i64;

        let mut req = AtomicModeReq::new();

        req.add_property(
            self.connector.handle(),
            self.props.conn_crtc_id,
            property::Value::CRTC(Some(self.crtc.handle())),
        );

        if mode_set {
            let mode_blob = self.d.create_property_blob(&self.mode)?;
            req.add_property(self.crtc.handle(), self.props.crtc_mode_id, mode_blob);
        }

        req.add_property(
            self.crtc.handle(),
            self.props.crtc_active,
            property::Value::Boolean(true),
        );

        req.add_property(
            self.plane,
            self.props.plane_fb_id,
            property::Value::Framebuffer(Some(fb.id)),
        );
        req.add_property(
            self.plane,
            self.props.plane_crtc_id,
            property::Value::CRTC(Some(self.crtc.handle())),
        );

        req.add_property(
            self.plane,
            self.props.plane_src_x,
            property::Value::UnsignedRange(0),
        );
        req.add_property(
            self.plane,
            self.props.plane_src_y,
            property::Value::UnsignedRange(0),
        );
        req.add_property(
            self.plane,
            self.props.plane_src_w,
            property::Value::UnsignedRange((fb_w as u64) << 16),
        );
        req.add_property(
            self.plane,
            self.props.plane_src_h,
            property::Value::UnsignedRange((fb_h as u64) << 16),
        );

        req.add_property(
            self.plane,
            self.props.plane_crtc_x,
            property::Value::SignedRange(crtc_x),
        );
        req.add_property(
            self.plane,
            self.props.plane_crtc_y,
            property::Value::SignedRange(crtc_y),
        );
        req.add_property(
            self.plane,
            self.props.plane_crtc_w,
            property::Value::UnsignedRange(crtc_w),
        );
        req.add_property(
            self.plane,
            self.props.plane_crtc_h,
            property::Value::UnsignedRange(crtc_h),
        );

        Ok(req)
    }

    fn pick_primary_plane(
        d: &DrmCard,
        res: &control::ResourceHandles,
        crtc: crtc::Handle,
    ) -> Result<plane::Handle> {
        let mut compatible = Vec::new();

        for plane_h in d.plane_handles()? {
            let plane_info = d.get_plane(plane_h)?;
            let compatible_crtcs = res.filter_crtcs(plane_info.possible_crtcs());

            if !compatible_crtcs.contains(&crtc) {
                continue;
            }

            let props = d.get_properties(plane_h)?;
            let mut is_primary = false;

            for (&prop_id, &val) in props.iter() {
                let info = d.get_property(prop_id)?;
                if info.name().to_str().map(|x| x == "type").unwrap_or(false) {
                    is_primary = val == control::PlaneType::Primary as u64;
                    break;
                }
            }

            if is_primary {
                return Ok(plane_h);
            }

            compatible.push(plane_h);
        }

        compatible
            .first()
            .copied()
            .ok_or_else(|| anyhow!("no plane compatible with the selected CRTC"))
    }

    fn atomic_commit(&self, flags: AtomicCommitFlags) -> Result<()> {
        if let Some(fb) = &self.fb {
            let req =
                self.build_atomic_req(fb, flags.contains(AtomicCommitFlags::ALLOW_MODESET))?;
            self.d.atomic_commit(flags, req).context("atomic commit")?;
            Ok(())
        } else {
            bail!("Framebuffer is in valid state")
        }
    }
}

impl Committer<Arc<DrmCard>, Rc<Framebuffer<DrmCard>>> for AtomicCommitter {
    fn init(d: Arc<DrmCard>) -> Result<Self> {
        d.enable_atomic()?;
        DrmDevice::set_client_capability(d.as_ref(), drm::ClientCapability::UniversalPlanes, true)
            .context("enabling DRM universal planes capability")?;

        let res = d.resource_handles()?;

        let con_handle = res
            .connectors()
            .iter()
            .copied()
            .find(|&conn| {
                d.get_connector(conn)
                    .map(|info| info.state() == connector::State::Connected)
                    .unwrap_or(false)
            })
            .context("no connected DRM connector found")?;

        let connector = d.get_connector(con_handle)?;
        let mode = connector
            .modes()
            .iter()
            .max_by(|a, b| {
                let a_size = a.size();
                let b_size = b.size();

                let a_area = (a_size.0 as u64) * (a_size.1 as u64);
                let b_area = (b_size.0 as u64) * (b_size.1 as u64);

                a_area
                    .cmp(&b_area)
                    .then_with(|| a.vrefresh().cmp(&b.vrefresh()))
            })
            .copied()
            .context("connector has no modes")?;

        connector.modes().iter().for_each(|x| {
            println!(
                "Available mode : {}x{}@{}",
                x.size().0,
                x.size().1,
                x.vrefresh()
            );
        });

        println!(
            "Selected mode : {}x{}@{}",
            mode.size().0,
            mode.size().1,
            mode.vrefresh()
        );

        let encoder_handle = connector
            .encoders()
            .first()
            .copied()
            .context("connector has no encoder")?;
        let encoder = d.get_encoder(encoder_handle)?;
        let crtc_handle = encoder.crtc().unwrap_or_else(|| res.crtcs()[0]);
        let crtc = d.get_crtc(crtc_handle)?;

        let plane = Self::pick_primary_plane(&d, &res, crtc.handle())?;

        let con_props = d
            .get_properties(connector.handle())?
            .as_hashmap(d.as_ref())?;
        let crtc_props = d.get_properties(crtc.handle())?.as_hashmap(d.as_ref())?;
        let plane_props = d.get_properties(plane)?.as_hashmap(d.as_ref())?;

        let props = AtomicProps {
            conn_crtc_id: con_props["CRTC_ID"].handle(),

            crtc_mode_id: crtc_props["MODE_ID"].handle(),
            crtc_active: crtc_props["ACTIVE"].handle(),

            plane_fb_id: plane_props["FB_ID"].handle(),
            plane_crtc_id: plane_props["CRTC_ID"].handle(),
            plane_src_x: plane_props["SRC_X"].handle(),
            plane_src_y: plane_props["SRC_Y"].handle(),
            plane_src_w: plane_props["SRC_W"].handle(),
            plane_src_h: plane_props["SRC_H"].handle(),
            plane_crtc_x: plane_props["CRTC_X"].handle(),
            plane_crtc_y: plane_props["CRTC_Y"].handle(),
            plane_crtc_w: plane_props["CRTC_W"].handle(),
            plane_crtc_h: plane_props["CRTC_H"].handle(),
        };

        let (tx, rx) = mpsc::sync_channel::<control::Event>(3);
        let rd_jh = d.clone();

        let jh = thread::spawn(move || -> Result<()> {
            println!("Event thread started.");
            loop {
                let events = rd_jh
                    .as_ref()
                    .receive_events()
                    .context("receiving DRM events")?;

                for ev in events {
                    tx.send(ev)?;
                }
            }
        });

        Ok(Self {
            d,
            fb: None,
            connector,
            mode,
            crtc,
            plane,
            props,
            srx: rx,
            event_handle: jh,
        })
    }
    fn awake(&self) -> Result<()> {
        self.atomic_commit(
            AtomicCommitFlags::ALLOW_MODESET
                | AtomicCommitFlags::PAGE_FLIP_EVENT
                | AtomicCommitFlags::NONBLOCK,
        )
    }
    fn commit(&self) -> Result<()> {
        self.atomic_commit(AtomicCommitFlags::PAGE_FLIP_EVENT | AtomicCommitFlags::NONBLOCK)
    }
    fn set_state(&mut self, state: Rc<Framebuffer<DrmCard>>) -> Result<()> {
        self.fb = Some(state);
        Ok(())
    }
    fn sync(&self) -> Result<()> {
        while let Ok(ev) = self.srx.recv() {
            match ev {
                control::Event::PageFlip(e) if e.crtc == self.crtc.handle() => {
                    return Ok(());
                }
                control::Event::Vblank(e) if e.crtc == self.crtc.handle() => {
                    return Ok(());
                }
                _ => continue,
            }
        }
        bail!("Event channel disconnected")
    }
}