medias 0.3.7

TV media player on bare DRM/KMS: Vulkan UI plane over libmpv video, for local files, UPnP/DLNA servers and YouTube, controlled by HDMI-CEC remote, evdev or terminal
//! Vulkan bring-up (milestone 2b): load the loader, enumerate devices, verify
//! the extensions the dmabuf-import render path needs, and create a logical
//! device with a graphics queue.
//!
//! Headless: no surface/swapchain. We render into dmabuf-backed images and
//! present through KMS, so no WSI extensions are enabled.

use std::ffi::{CStr, c_char};

use ash::{Entry, Instance, vk};

mod render;
mod target;
mod text;
#[allow(unused_imports)] // opaque handles used via Gpu methods
pub use render::Renderer;
#[allow(unused_imports)]
pub use target::Target;
#[allow(unused_imports)]
pub use text::{Frame, Text, Thumb, THUMB_H, THUMB_SLOTS, THUMB_W};
pub(crate) use text::{slot_origin, Quad, THUMB_ATLAS_H, THUMB_ATLAS_W};

/// Device extensions required to import a GBM dmabuf as a renderable VkImage and
/// synchronise with KMS.
fn required() -> [&'static CStr; 4] {
    [
        ash::khr::external_memory_fd::NAME,
        ash::ext::external_memory_dma_buf::NAME,
        ash::ext::image_drm_format_modifier::NAME,
        ash::khr::external_semaphore_fd::NAME,
    ]
}

#[allow(dead_code)] // entry/physical/queue are used by the render path (2c)
pub struct Gpu {
    pub entry: Entry,
    pub instance: Instance,
    pub physical: vk::PhysicalDevice,
    pub device: ash::Device,
    pub queue_family: u32,
    pub queue: vk::Queue,
    pub name: String,
}

impl Gpu {
    /// Create an instance, pick a device that supports the full import path (a
    /// graphics queue + all required extensions), and create it.
    pub fn new() -> Result<Gpu, String> {
        let entry = unsafe { Entry::load() }.map_err(|e| format!("load libvulkan: {e}"))?;

        let app = vk::ApplicationInfo::default().api_version(vk::API_VERSION_1_3);
        let ci = vk::InstanceCreateInfo::default().application_info(&app);
        let instance =
            unsafe { entry.create_instance(&ci, None) }.map_err(|e| format!("instance: {e}"))?;

        let result = (|| {
            let physicals = unsafe { instance.enumerate_physical_devices() }
                .map_err(|e| format!("enumerate: {e}"))?;

            let mut chosen = None;
            for pd in physicals {
                let props = unsafe { instance.get_physical_device_properties(pd) };
                let name = cstr(&props.device_name);
                let exts = unsafe { instance.enumerate_device_extension_properties(pd) }
                    .unwrap_or_default();
                let have: Vec<&CStr> = exts
                    .iter()
                    .map(|e| unsafe { CStr::from_ptr(e.extension_name.as_ptr()) })
                    .collect();
                let missing: Vec<&str> = required()
                    .iter()
                    .filter(|r| !have.contains(r))
                    .map(|r| r.to_str().unwrap_or("?"))
                    .collect();
                let gfx = graphics_family(&instance, pd);

                println!(
                    "  gpu: {name}  [{:?}]  api {}  gfxq={:?}  import-exts={}",
                    props.device_type,
                    api_str(props.api_version),
                    gfx,
                    if missing.is_empty() {
                        "all".into()
                    } else {
                        format!("MISSING {missing:?}")
                    }
                );

                if chosen.is_none() && missing.is_empty() {
                    if let Some(q) = gfx {
                        chosen = Some((pd, q, name));
                    }
                }
            }

            let (physical, queue_family, name) =
                chosen.ok_or("no device supports the dmabuf import path")?;

            let prio = [1.0f32];
            let qci = [vk::DeviceQueueCreateInfo::default()
                .queue_family_index(queue_family)
                .queue_priorities(&prio)];
            let ext_ptrs: Vec<*const c_char> = required().iter().map(|c| c.as_ptr()).collect();
            let dci = vk::DeviceCreateInfo::default()
                .queue_create_infos(&qci)
                .enabled_extension_names(&ext_ptrs);
            let device = unsafe { instance.create_device(physical, &dci, None) }
                .map_err(|e| format!("create device: {e}"))?;
            let queue = unsafe { device.get_device_queue(queue_family, 0) };

            Ok(Gpu {
                entry: entry.clone(),
                instance: instance.clone(),
                physical,
                device,
                queue_family,
                queue,
                name,
            })
        })();

        if result.is_err() {
            unsafe { instance.destroy_instance(None) };
        }
        result
    }
}

impl Drop for Gpu {
    fn drop(&mut self) {
        unsafe {
            self.device.destroy_device(None);
            self.instance.destroy_instance(None);
        }
    }
}

fn graphics_family(instance: &Instance, pd: vk::PhysicalDevice) -> Option<u32> {
    let families = unsafe { instance.get_physical_device_queue_family_properties(pd) };
    families
        .iter()
        .position(|f| f.queue_flags.contains(vk::QueueFlags::GRAPHICS))
        .map(|i| i as u32)
}

fn cstr(raw: &[c_char]) -> String {
    unsafe { CStr::from_ptr(raw.as_ptr()) }
        .to_string_lossy()
        .into_owned()
}

fn api_str(v: u32) -> String {
    format!(
        "{}.{}.{}",
        vk::api_version_major(v),
        vk::api_version_minor(v),
        vk::api_version_patch(v)
    )
}