edgefirst-image 0.35.0

High-performance image processing with hardware acceleration for edge AI
Documentation
// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
// SPDX-License-Identifier: Apache-2.0

//! Linux implementation of [`GlPlatform`]: EGL display bring-up via
//! GBM/PlatformDevice/Default and DMA-BUF buffer import.
//!
//! Pure delegation — the actual machinery predates this seam and lives in
//! [`super::super::context`] (display/context bring-up, transfer-backend
//! probe) and [`super::super::dma_import`] (DMA-BUF attribute assembly).
//! This file only binds it to the cross-platform contract; it must stay
//! free of logic so the platform trait remains the single source of truth
//! for *what* a platform provides, with `context.rs`/`dma_import.rs`
//! owning *how* Linux provides it.

use super::super::context::{egl_ext, Egl, GlContext};
use super::super::dma_import::DmaImportAttrs;
use super::super::EglDisplayKind;
use super::GlPlatform;
use edgefirst_egl as egl;
use edgefirst_tensor::{PixelFormat, Tensor, TensorDyn};
use std::rc::Rc;

/// An owned EGLImage import over a DMA-BUF. Dropping destroys the
/// EGLImage under the same display-level lifecycle lock as creation.
/// This is the Linux [`GlPlatform::Import`]; the macOS analogue is
/// `angle::IoSurfacePbuffer`.
pub(in crate::opengl_headless) struct EglImage {
    pub(in crate::opengl_headless) egl_image: egl::Image,
    pub(in crate::opengl_headless) egl: Rc<Egl>,
    pub(in crate::opengl_headless) display: egl::Display,
    /// Texel the logical image starts at inside this import, and the
    /// import's own texel size — both `(0, 0)` / `None` unless a source was
    /// rebased onto an aligned DMA-BUF offset (issue #170).
    pub(in crate::opengl_headless) sample_origin_px: (u32, u32),
    pub(in crate::opengl_headless) sample_extent_px: Option<(u32, u32)>,
}

impl Drop for EglImage {
    fn drop(&mut self) {
        if self.egl_image.as_ptr() == egl::NO_IMAGE {
            return;
        }

        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            // Display-level EGL op — same dedicated lock as creation.
            let _image_guard = super::super::context::image_lifecycle_guard();
            // Adreno unmaps the import's driver mapping twice inside one
            // eglDestroyImage; no tensor mapping may be created meanwhile.
            let e = edgefirst_tensor::pin::with_cpu_mappings_excluded(|| {
                GlContext::egl_destroy_image_with_fallback(&self.egl, self.display, self.egl_image)
            });
            if let Err(e) = e {
                log::error!("Could not destroy EGL image: {e:?}");
            }
        }));
    }
}

/// Marker type: Linux EGL + DMA-BUF platform. Stateless — all state
/// lives in the [`GlContext`] created by [`GlPlatform::init_display`].
pub(crate) struct LinuxEgl;

impl GlPlatform for LinuxEgl {
    type Display = GlContext;
    type Import = EglImage;
    type ImportHandle = egl::Image;

    // EGLImage targets persist on the texture object — the engine's
    // binding-skip cache (BufferImportKey on Texture) applies.
    const PERSISTENT_TEX_BINDINGS: bool = true;

    /// The planar F16 zero-copy render only -- the one float path this leaf
    /// has ever executed. The rest were written and measured on Windows.
    const ZERO_COPY_FLOAT: super::super::float_dispatch::ZeroCopyFloatSet =
        super::super::float_dispatch::ZeroCopyFloatSet::PlanarF16;
    const EXTERNAL_OES: bool = true;
    const NV_CHROMA_IMPORT: bool = true;

    fn init_display(kind: Option<EglDisplayKind>) -> crate::Result<GlContext> {
        GlContext::new(kind)
    }

    fn load_gl_once(display: &GlContext) {
        static GL_LOADED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
        GL_LOADED.get_or_init(|| {
            edgefirst_gl::load_with(|s| {
                display
                    .egl
                    .get_proc_address(s)
                    .map_or(std::ptr::null(), |p| p as *const _)
            });
        });
    }

    fn import_handle(import: &EglImage) -> egl::Image {
        import.egl_image
    }

    fn import_extent(import: &EglImage) -> Option<(u32, u32)> {
        // The DMA-BUF EGLImage is created at the tensor's logical size --
        // the buffer's physical pitch is a separate import attribute -- so
        // this is `None` except for a source rebased onto an aligned offset
        // (issue #170), which imports `x_shift_px` texels wider.
        import.sample_extent_px
    }

    fn import_origin(import: &EglImage) -> (u32, u32) {
        import.sample_origin_px
    }

    unsafe fn attach_tex_image_2d(_display: &GlContext, handle: egl::Image) -> crate::Result<()> {
        unsafe {
            edgefirst_gl::gl::EGLImageTargetTexture2DOES(
                edgefirst_gl::gl::TEXTURE_2D,
                handle.as_ptr(),
            );
        }
        Ok(())
    }

    unsafe fn attach_tex_image_external(
        _display: &GlContext,
        handle: egl::Image,
    ) -> crate::Result<()> {
        edgefirst_gl::egl_image_target_texture_2d_oes(
            edgefirst_gl::gl::TEXTURE_EXTERNAL_OES,
            handle.as_ptr(),
        );
        Ok(())
    }

    unsafe fn attach_renderbuffer_storage(
        _display: &GlContext,
        handle: egl::Image,
    ) -> crate::Result<()> {
        unsafe {
            edgefirst_gl::gl::EGLImageTargetRenderbufferStorageOES(
                edgefirst_gl::gl::RENDERBUFFER,
                handle.as_ptr(),
            );
        }
        Ok(())
    }

    fn begin_gpu_pass(_display: &GlContext) {
        // Each processor's context is current on its own thread and the
        // driver keeps per-context state; nothing to re-sync.
    }

    fn end_gpu_pass(_display: &GlContext) {
        // EGLImage texture targets persist by design; nothing to release.
    }

    fn native_fence_sync(_display: &GlContext) -> bool {
        // Some Linux drivers do expose EGL_ANDROID_native_fence_sync, but
        // no Linux consumer takes a sync-fence fd today — out of scope.
        false
    }

    fn export_completion_fence(
        _display: &GlContext,
        _recorded: Option<u64>,
    ) -> crate::Result<Option<super::super::CompletionFence>> {
        Ok(None)
    }

    fn record_completion(_display: &GlContext, _dst: &mut TensorDyn, _submit: bool) -> Option<u64> {
        // No device fence on Linux — convert() returning means the GPU
        // work is complete.
        None
    }

    fn submit_recorded(_display: &GlContext) {
        // Nothing is ever queued unsubmitted: there is no device fence.
    }

    fn import_buffer_packed<T>(
        display: &GlContext,
        img: &Tensor<T>,
        width: usize,
        height: usize,
        fmt: super::PackedImportFormat,
    ) -> crate::Result<EglImage>
    where
        T: num_traits::Num + Clone + std::fmt::Debug + Send + Sync + edgefirst_tensor::Element,
    {
        use std::os::fd::AsRawFd;
        let drm_format = match fmt {
            super::PackedImportFormat::Rgba8888 => drm_fourcc::DrmFourcc::Abgr8888,
            super::PackedImportFormat::Rgba16161616F => drm_fourcc::DrmFourcc::Abgr16161616f,
            super::PackedImportFormat::Rgba32323232F => {
                return Err(crate::Error::NotSupported(
                    "RGBA32F zero-copy import has no buffer format on this platform".into(),
                ))
            }
        };
        let bpp = fmt.bytes_per_pixel();
        // `dmabuf()`, not `as_dma().fd` -- see `gl/dma_import.rs`'s own
        // note: `as_dma` is `None` on the `dynamic` backend for every
        // tensor, DMA-backed or not.
        let fd = img
            .dmabuf()
            .map_err(|e| {
                crate::Error::NotImplemented(format!(
                    "import_buffer_packed requires DMA tensor ({e})"
                ))
            })?
            .as_raw_fd();

        // Use the tensor's stored stride when available (externally
        // allocated buffers with row padding), otherwise compute the
        // tightly-packed pitch.
        let pitch = img.effective_row_stride().unwrap_or(width * bpp);
        let offset = img.plane_offset().unwrap_or(0);
        let egl_img_attr = [
            egl_ext::LINUX_DRM_FOURCC as egl::Attrib,
            drm_format as u32 as egl::Attrib,
            edgefirst_egl::WIDTH as egl::Attrib,
            width as egl::Attrib,
            edgefirst_egl::HEIGHT as egl::Attrib,
            height as egl::Attrib,
            egl_ext::DMA_BUF_PLANE0_PITCH as egl::Attrib,
            pitch as egl::Attrib,
            egl_ext::DMA_BUF_PLANE0_OFFSET as egl::Attrib,
            offset as egl::Attrib,
            egl_ext::DMA_BUF_PLANE0_FD as egl::Attrib,
            fd as egl::Attrib,
            egl::IMAGE_PRESERVED as egl::Attrib,
            egl::TRUE as egl::Attrib,
            edgefirst_egl::NONE as egl::Attrib,
        ];
        new_egl_image_owned(display, egl_ext::LINUX_DMA_BUF, &egl_img_attr)
    }

    fn import_buffer(
        display: &GlContext,
        img: &Tensor<u8>,
        fmt: PixelFormat,
        for_dst: bool,
    ) -> crate::Result<EglImage> {
        let attrs = DmaImportAttrs::from_tensor(img, fmt, for_dst)?;
        let mut image =
            new_egl_image_owned(display, egl_ext::LINUX_DMA_BUF, &attrs.to_egl_attribs())?;
        // A source rebased onto an aligned base imports wider than the
        // logical image and starts it `x_shift_px` texels in; the engine
        // folds both through `import_extent` / `import_origin`. Zero for
        // every other import, which then reports the logical image as
        // before. The extent travels WITH the origin: the fold ignores an
        // origin whose map has no extent, which would put the shift nowhere
        // and show up only as a black frame on the one board that rebases.
        if attrs.x_shift_px != 0 {
            image.sample_origin_px = (attrs.x_shift_px, 0);
            image.sample_extent_px = Some((attrs.width as u32, attrs.height as u32));
        }
        Ok(image)
    }

    fn import_buffer_nv_r8(
        display: &GlContext,
        img: &Tensor<u8>,
        fmt: PixelFormat,
    ) -> crate::Result<EglImage> {
        let attrs = DmaImportAttrs::from_tensor_nv_r8(img, fmt)?;
        new_egl_image_owned(display, egl_ext::LINUX_DMA_BUF, &attrs.to_egl_attribs())
    }

    fn import_buffer_nv_chroma(
        display: &GlContext,
        img: &Tensor<u8>,
        fmt: PixelFormat,
    ) -> crate::Result<EglImage> {
        let attrs = DmaImportAttrs::from_tensor_nv_chroma_gr88(img, fmt)?;
        new_egl_image_owned(display, egl_ext::LINUX_DMA_BUF, &attrs.to_egl_attribs())
    }
}

/// Create an owned EGLImage from raw EGL import attributes.
///
/// Every EGLImage creation funnels through here (DMA-BUF, NV R8, and the
/// float path's RGB renderbuffer import), so one span = one actual
/// `eglCreateImageKHR`. Steady-state frame loops must show ZERO of these
/// after warmup — the span count is the observable for cache-behavior
/// equality gates.
///
/// Module-scoped (not a trait method): the attribute list is
/// EGL/DRM-specific, so only Linux-side code (the float path's
/// dims-override import in `processor/mod.rs`) may call it directly;
/// portable code goes through [`GlPlatform::import_buffer`].
pub(in crate::opengl_headless) fn new_egl_image_owned(
    display: &GlContext,
    target: egl::Enum,
    attrib_list: &[egl::Attrib],
) -> crate::Result<EglImage> {
    let _span = tracing::trace_span!("image.convert.gl.egl_import", target).entered();
    // EGLImage creation is a display-level EGL op shared by every
    // processor — serialized by a dedicated short lock (zero cost in
    // steady state: creations are cache-miss-only).
    let _image_guard = super::super::context::image_lifecycle_guard();
    let image = GlContext::egl_create_image_with_fallback(
        &display.egl,
        display.display,
        unsafe { egl::Context::from_ptr(egl::NO_CONTEXT) },
        target,
        unsafe { egl::ClientBuffer::from_ptr(std::ptr::null_mut()) },
        attrib_list,
    )?;
    Ok(EglImage {
        egl_image: image,
        display: display.display,
        egl: Rc::clone(&display.egl),
        sample_origin_px: (0, 0),
        sample_extent_px: None,
    })
}