edgefirst-image 0.38.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

//! Portable renderer helpers shared by both platform backends.
//!
//! Pure, platform-neutral logic (no `gbm`/DMA-BUF or IOSurface types) that both
//! the Linux (`gl::processor`) and macOS (`gl::macos_processor`) backends call,
//! so the shared GL render logic has one definition. Compiled on every platform
//! (the `gl` module is feature-gated on `opengl`, not on the OS).

use crate::{Error, ResolvedCrop};
use std::ffi::CString;

/// Compile a GLSL shader of `kind` from source; returns the shader id.
///
/// On failure the shader is deleted and an [`Error::OpenGl`] carrying the
/// driver info-log is returned. The caller must have a current GL context.
///
/// # Safety
/// Requires a current GL context.
unsafe fn compile_shader(kind: u32, src: &str) -> crate::Result<u32> {
    unsafe {
        let shader = edgefirst_gl::gl::CreateShader(kind);
        let c = CString::new(src).map_err(|e| Error::OpenGl(format!("shader CString: {e}")))?;
        let ptr = c.as_ptr();
        let len = src.len() as i32;
        edgefirst_gl::gl::ShaderSource(shader, 1, &ptr, &len);
        edgefirst_gl::gl::CompileShader(shader);
        let mut ok = 0i32;
        edgefirst_gl::gl::GetShaderiv(shader, edgefirst_gl::gl::COMPILE_STATUS, &mut ok);
        if ok == 0 {
            let mut log = [0u8; 4096];
            let mut log_len = 0i32;
            edgefirst_gl::gl::GetShaderInfoLog(
                shader,
                log.len() as i32,
                &mut log_len,
                log.as_mut_ptr() as *mut _,
            );
            let msg = String::from_utf8_lossy(&log[..log_len.max(0) as usize]).into_owned();
            edgefirst_gl::gl::DeleteShader(shader);
            return Err(Error::OpenGl(format!(
                "shader compile failed (kind=0x{kind:x}): {msg}"
            )));
        }
        Ok(shader)
    }
}

/// Compile + link a vertex/fragment program from source; returns the program id.
///
/// Shared by both backends. The two shaders are detached and deleted after a
/// successful link (GL frees them when the program is deleted), so the returned
/// program id is the only resource the caller must track. On any error the
/// partially-built shaders/program are cleaned up.
///
/// # Safety
/// Requires a current GL context.
pub(super) unsafe fn compile_program(vertex_src: &str, fragment_src: &str) -> crate::Result<u32> {
    unsafe {
        let vs = compile_shader(edgefirst_gl::gl::VERTEX_SHADER, vertex_src)?;
        // Own `vs`/`fs`/`program` so any early return cleans them up.
        struct ProgramBuild {
            vs: Option<u32>,
            fs: Option<u32>,
            program: Option<u32>,
        }
        impl Drop for ProgramBuild {
            fn drop(&mut self) {
                unsafe {
                    if let Some(p) = self.program {
                        edgefirst_gl::gl::DeleteProgram(p);
                    }
                    if let Some(s) = self.fs {
                        edgefirst_gl::gl::DeleteShader(s);
                    }
                    if let Some(s) = self.vs {
                        edgefirst_gl::gl::DeleteShader(s);
                    }
                }
            }
        }
        let mut state = ProgramBuild {
            vs: Some(vs),
            fs: None,
            program: None,
        };

        let fs = compile_shader(edgefirst_gl::gl::FRAGMENT_SHADER, fragment_src)?;
        state.fs = Some(fs);

        let program = edgefirst_gl::gl::CreateProgram();
        state.program = Some(program);
        edgefirst_gl::gl::AttachShader(program, vs);
        edgefirst_gl::gl::AttachShader(program, fs);
        edgefirst_gl::gl::LinkProgram(program);
        let mut ok = 0i32;
        edgefirst_gl::gl::GetProgramiv(program, edgefirst_gl::gl::LINK_STATUS, &mut ok);
        if ok == 0 {
            let mut log = [0u8; 4096];
            let mut log_len = 0i32;
            edgefirst_gl::gl::GetProgramInfoLog(
                program,
                log.len() as i32,
                &mut log_len,
                log.as_mut_ptr() as *mut _,
            );
            let msg = String::from_utf8_lossy(&log[..log_len.max(0) as usize]).into_owned();
            return Err(Error::OpenGl(format!("program link failed: {msg}")));
        }

        // Success: detach + delete shaders, disarm cleanup, return the program.
        edgefirst_gl::gl::DeleteShader(state.vs.take().unwrap());
        edgefirst_gl::gl::DeleteShader(state.fs.take().unwrap());
        let program = state.program.take().unwrap();
        std::mem::forget(state);
        Ok(program)
    }
}

/// Set `MIN`/`MAG` filtering on the currently-bound texture of `target`.
///
/// Leaves `WRAP_S`/`WRAP_T` at the GL default (`REPEAT`) — correct for textures
/// sampled strictly inside `[0,1]` (the render-quad sources here). Use
/// [`set_tex_filter_clamp`] only where the original code also set the wrap mode.
///
/// # Safety
/// Requires a current GL context and a bound texture on `target`.
pub(super) unsafe fn set_tex_filter(target: u32, filter: u32) {
    unsafe {
        edgefirst_gl::gl::TexParameteri(
            target,
            edgefirst_gl::gl::TEXTURE_MIN_FILTER,
            filter as i32,
        );
        edgefirst_gl::gl::TexParameteri(
            target,
            edgefirst_gl::gl::TEXTURE_MAG_FILTER,
            filter as i32,
        );
    }
}

/// Set `MIN`/`MAG` filtering and `CLAMP_TO_EDGE` `WRAP_S`/`WRAP_T` on the
/// currently-bound texture of `target` (the four-call cluster).
///
/// # Safety
/// Requires a current GL context and a bound texture on `target`.
pub(super) unsafe fn set_tex_filter_clamp(target: u32, filter: u32) {
    unsafe {
        set_tex_filter(target, filter);
        edgefirst_gl::gl::TexParameteri(
            target,
            edgefirst_gl::gl::TEXTURE_WRAP_S,
            edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
        );
        edgefirst_gl::gl::TexParameteri(
            target,
            edgefirst_gl::gl::TEXTURE_WRAP_T,
            edgefirst_gl::gl::CLAMP_TO_EDGE as i32,
        );
    }
}

/// Check that the currently-bound draw framebuffer is complete.
///
/// Returns `Err(status)` with the raw `GL_FRAMEBUFFER_*` status code when the
/// framebuffer is not `FRAMEBUFFER_COMPLETE`, so each backend can format its own
/// context-specific error (and, on Linux, unbind before falling back to CPU).
///
/// # Safety
/// Requires a current GL context with a framebuffer bound to `FRAMEBUFFER`.
pub(super) unsafe fn check_framebuffer_complete() -> Result<(), u32> {
    unsafe {
        let status = edgefirst_gl::gl::CheckFramebufferStatus(edgefirst_gl::gl::FRAMEBUFFER);
        if status == edgefirst_gl::gl::FRAMEBUFFER_COMPLETE {
            Ok(())
        } else {
            Err(status)
        }
    }
}

/// Crop-derived float render uniforms shared by both float render paths.
///
/// Computes `(src_rect_uv, dst_rect_px, pad_color)` from `crop` after running
/// [`Crop::check_crop_dims`] (the dimension validation must happen first, as it
/// can error). `src_rect_uv` is normalized to source dims; `dst_rect_px` is in
/// single-plane pixel coords; `pad_color` is normalized `[0,1]` RGBA. When a
/// rect is `None` the whole image is sampled/painted (identity transform).
pub(super) fn float_crop_uniforms(
    crop: &ResolvedCrop,
    src_w: usize,
    src_h: usize,
    dst_w: usize,
    dst_h: usize,
) -> crate::Result<([f32; 4], [f32; 4], [f32; 4])> {
    crop.check_crop_dims(src_w, src_h, dst_w, dst_h)?;
    let src_rect_uv = match crop.src_rect {
        Some(r) => [
            r.left as f32 / src_w as f32,
            r.top as f32 / src_h as f32,
            r.width as f32 / src_w as f32,
            r.height as f32 / src_h as f32,
        ],
        None => [0.0, 0.0, 1.0, 1.0],
    };
    let dst_rect_px = match crop.dst_rect {
        Some(r) => [r.left as f32, r.top as f32, r.width as f32, r.height as f32],
        None => [0.0, 0.0, dst_w as f32, dst_h as f32],
    };
    // Four components: a `PlanarRgba` or `Rgba` float destination pads its
    // alpha plane too, with the same value the CPU reference writes there.
    // A crop with no pad colour also has no destination rect, so the whole
    // destination is content and the value below is never sampled; opaque
    // black is the sane default for it.
    let pad_color = match crop.dst_color {
        Some([r, g, b, a]) => [
            r as f32 / 255.0,
            g as f32 / 255.0,
            b as f32 / 255.0,
            a as f32 / 255.0,
        ],
        None => [0.0, 0.0, 0.0, 1.0],
    };
    Ok((src_rect_uv, dst_rect_px, pad_color))
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
    use super::float_crop_uniforms;
    use crate::{Rect, ResolvedCrop};

    #[test]
    fn identity_crop_is_full_image() {
        let (src_uv, dst_px, pad) =
            float_crop_uniforms(&ResolvedCrop::no_crop(), 640, 480, 320, 240).unwrap();
        assert_eq!(src_uv, [0.0, 0.0, 1.0, 1.0]);
        assert_eq!(dst_px, [0.0, 0.0, 320.0, 240.0]);
        assert_eq!(pad, [0.0, 0.0, 0.0, 1.0]);
    }

    #[test]
    fn src_rect_normalizes_to_source_dims() {
        let crop = ResolvedCrop {
            src_rect: Some(Rect::new(160, 120, 320, 240)),
            ..ResolvedCrop::no_crop()
        };
        let (src_uv, _, _) = float_crop_uniforms(&crop, 640, 480, 320, 240).unwrap();
        assert_eq!(src_uv, [0.25, 0.25, 0.5, 0.5]);
    }

    #[test]
    fn dst_rect_is_pixel_coords_and_pad_color_normalizes() {
        let crop = ResolvedCrop {
            dst_rect: Some(Rect::new(10, 20, 100, 50)),
            dst_color: Some([255, 128, 0, 255]),
            ..ResolvedCrop::no_crop()
        };
        let (_, dst_px, pad) = float_crop_uniforms(&crop, 640, 480, 320, 240).unwrap();
        assert_eq!(dst_px, [10.0, 20.0, 100.0, 50.0]);
        assert_eq!(pad, [1.0, 128.0 / 255.0, 0.0, 1.0]);
    }

    #[test]
    fn out_of_bounds_src_rect_errors() {
        // src_rect exceeds the source extent → check_crop_dims rejects it first.
        let crop = ResolvedCrop {
            src_rect: Some(Rect::new(6, 0, 4, 4)), // 6 + 4 = 10 > src_w = 8
            ..ResolvedCrop::no_crop()
        };
        let err = float_crop_uniforms(&crop, 8, 8, 8, 8).unwrap_err();
        assert!(
            matches!(err, crate::Error::CropInvalid(_)),
            "expected CropInvalid, got {err:?}"
        );
    }
}