medias 0.4.0

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
//! UI plane: double-buffered ARGB8888 scanout buffers filled from a `Frame`.
//! Vulkan renders into imported dmabufs; without a Vulkan device with dmabuf
//! import (e.g. Mali Midgard), or with MEDIAS_UI=cpu, `raster::Canvas` fills
//! KMS dumb buffers.

pub(crate) mod raster;

use drm::buffer::DrmFourcc;
use drm::control::dumbbuffer::DumbBuffer;
use drm::control::{Device as ControlDevice, framebuffer};

use crate::gbm::{Allocator, Scanout};
use crate::kms::{Card, Output};
use crate::vk::{self, Frame, Text, Thumb};
use raster::Canvas;

enum Backend {
    Vk { gpu: vk::Gpu, targets: Vec<vk::Target>, fbs: Vec<ash::vk::Framebuffer> },
    Cpu { canvas: Canvas, dumbs: Vec<DumbBuffer> },
}

pub struct UiPlane {
    fbs: Vec<framebuffer::Handle>, // KMS framebuffers, one per buffer
    scanouts: Vec<Scanout>,        // Vulkan: GBM buffers behind `fbs`
    backend: Backend,
}

impl UiPlane {
    /// `n` buffers of `w`x`h`, plus the text atlas the backend samples.
    pub fn new(alloc: &Allocator, card: &Card, output: &Output, w: u32, h: u32, n: usize) -> Result<(UiPlane, Text), String> {
        if std::env::var("MEDIAS_UI").is_ok_and(|v| v.trim() == "cpu") {
            println!("ui: CPU rasterizer (MEDIAS_UI=cpu)");
        } else {
            match Self::vulkan(alloc, card, output, w, h, n) {
                Ok(r) => return Ok(r),
                Err(e) => println!("ui: Vulkan unavailable ({e}); CPU rasterizer"),
            }
        }
        let text = Text::new()?;
        // KMS dumb buffers: Mesa's GBM allocates ARGB8888 with GBM_BO_USE_WRITE
        // only for cursors (EINVAL for scanout, e.g. Mesa on RK3399 Debian).
        let mut plane = UiPlane {
            fbs: Vec::new(),
            scanouts: Vec::new(),
            backend: Backend::Cpu { canvas: Canvas::new(w, h), dumbs: Vec::new() },
        };
        for _ in 0..n {
            let Backend::Cpu { dumbs, .. } = &mut plane.backend else { unreachable!() };
            let step = card.create_dumb_buffer((w, h), DrmFourcc::Argb8888, 32).and_then(|db| {
                // Legacy AddFB: depth 32, bpp 32 is ARGB8888.
                match card.add_framebuffer(&db, 32, 32) {
                    Ok(fb) => Ok((db, fb)),
                    Err(e) => {
                        let _ = card.destroy_dumb_buffer(db);
                        Err(e)
                    }
                }
            });
            match step {
                Ok((db, fb)) => {
                    dumbs.push(db);
                    plane.fbs.push(fb);
                }
                Err(e) => {
                    plane.destroy(text, card);
                    return Err(format!("ui dumb buffer: {e}"));
                }
            }
        }
        Ok((plane, text))
    }

    fn vulkan(alloc: &Allocator, card: &Card, output: &Output, w: u32, h: u32, n: usize) -> Result<(UiPlane, Text), String> {
        let gpu = vk::Gpu::new()?;
        let text = gpu.create_text()?;
        let mut plane = UiPlane {
            fbs: Vec::new(),
            scanouts: Vec::new(),
            backend: Backend::Vk { gpu, targets: Vec::new(), fbs: Vec::new() },
        };
        for _ in 0..n {
            let Backend::Vk { gpu, targets, fbs } = &mut plane.backend else { unreachable!() };
            let step = gpu.create_scanout_target(alloc, card, output, w, h, DrmFourcc::Argb8888).and_then(|(buf, target)| {
                match text.framebuffer(gpu, &target) {
                    Ok(fb) => Ok((buf, target, fb)),
                    Err(e) => {
                        gpu.destroy_target(target);
                        let _ = card.destroy_framebuffer(buf.fb);
                        Err(e)
                    }
                }
            });
            match step {
                Ok((buf, target, fb)) => {
                    plane.fbs.push(buf.fb);
                    plane.scanouts.push(buf);
                    targets.push(target);
                    fbs.push(fb);
                }
                Err(e) => {
                    plane.destroy(text, card);
                    return Err(e);
                }
            }
        }
        if let Backend::Vk { gpu, .. } = &plane.backend {
            println!("ui: Vulkan on {}", gpu.name);
        }
        Ok((plane, text))
    }

    /// KMS framebuffer of buffer `i`.
    pub fn fb(&self, i: usize) -> framebuffer::Handle {
        self.fbs[i]
    }

    pub fn upload_thumb(&mut self, text: &Text, slot: usize, w: u32, h: u32, rgba: &[u8]) -> Result<Thumb, String> {
        match &mut self.backend {
            Backend::Vk { gpu, .. } => text.upload_thumb(gpu, slot, w, h, rgba),
            Backend::Cpu { canvas, .. } => canvas.upload_thumb(slot, w, h, rgba),
        }
    }

    /// Draw `frame` into buffer `i` and wait for completion.
    pub fn render(&mut self, card: &Card, text: &Text, i: usize, frame: &Frame) -> Result<(), String> {
        match &mut self.backend {
            Backend::Vk { gpu, targets, fbs } => text.render(gpu, &targets[i], fbs[i], frame),
            Backend::Cpu { canvas, dumbs } => {
                canvas.render(text, frame);
                let db = &mut dumbs[i];
                let stride = drm::buffer::Buffer::pitch(db) as usize;
                let mut map = card.map_dumb_buffer(db).map_err(|e| format!("ui map: {e}"))?;
                canvas.write(map.as_mut(), stride);
                Ok(())
            }
        }
    }

    pub fn destroy(self, text: Text, card: &Card) {
        for fb in self.fbs {
            let _ = card.destroy_framebuffer(fb);
        }
        match self.backend {
            Backend::Vk { gpu, targets, fbs } => {
                for fb in fbs {
                    gpu.destroy_framebuffer(fb);
                }
                text.destroy(&gpu);
                for t in targets {
                    gpu.destroy_target(t);
                }
                drop(self.scanouts);
            }
            Backend::Cpu { dumbs, .. } => {
                for db in dumbs {
                    let _ = card.destroy_dumb_buffer(db);
                }
            }
        }
    }
}