Skip to main content

edgefirst_image/gl/
mod.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4#![cfg(any(
5    target_os = "linux",
6    target_os = "macos",
7    target_os = "ios",
8    target_os = "android"
9))]
10#![cfg(feature = "opengl")]
11// Several types defined at the `gl` module root (EglDisplayKind,
12// TransferBackend, RegionOfInterest, etc.) are consumed only by the
13// Linux-only inner modules (`context`, `dma_import`, `fourcc`). The
14// macOS/iOS and Android paths import platform buffers through their own
15// modules (`iosurface_import`, `ahardwarebuffer_import`) and do not
16// touch every shared type, so some appear unused off Linux.
17// Rather than fragmenting the type definitions per platform, suppress
18// the dead-code/unused-import lints on non-Linux targets.
19#![cfg_attr(not(target_os = "linux"), allow(dead_code, unused_imports))]
20
21// Module layout:
22//   - `platform/` — cross-platform display/EGL-loader seam (all OSes)
23//   - Linux-only:   `context`, `dma_import`, `fourcc`
24//   - macOS/iOS:    `iosurface_import`
25//   - Android-only: `ahardwarebuffer_import`
26// The engine (`processor`, `threaded`, `cache`, `resources`, `shaders`)
27// is portable and reaches platform buffers only through the `GlPlatform`
28// trait — see `crates/image/ARCHITECTURE.md`.
29
30macro_rules! function {
31    () => {{
32        fn f() {}
33        fn type_name_of<T>(_: T) -> &'static str {
34            std::any::type_name::<T>()
35        }
36        let name = type_name_of(f);
37
38        // Find and cut the rest of the path
39        match &name[..name.len() - 3].rfind(':') {
40            Some(pos) => &name[pos + 1..name.len() - 3],
41            None => &name[..name.len() - 3],
42        }
43    }};
44}
45
46mod cache;
47#[cfg(target_os = "linux")]
48mod context;
49#[cfg(target_os = "linux")]
50mod dma_import;
51// Cfg-agnostic: the float render-path classifier is the single source of
52// truth for the "(PixelFormat, DType, TensorMemory) → float path" decision and
53// is compiled on every platform so both the Linux and macOS backends share one
54// definition (see `crates/image/ARCHITECTURE.md`, review item #4).
55// Portable renderer helpers (crop uniforms, …) shared by both platform
56// backends. No gbm/IOSurface types; compiled on both.
57mod core;
58mod float_dispatch;
59// Pure decision table for the proto-segmentation render path (upload
60// strategy × program × count uniform). No GL types; host-tested
61// exhaustively. See `proto_dispatch.rs`.
62mod proto_dispatch;
63// Portable GL render lowering (y-flip viewport, source UV, batch chunk planner).
64// No platform types; compiled on both platforms, consumed by the converged
65// tile/batch renderer. See `render.rs`.
66mod render;
67// PixelFormat -> DRM FourCC mapping via the portable `drm_fourcc` crate (NOT
68// `gbm`). DRM FourCC is a Linux/DMA-BUF concept, so this lives with the other
69// Linux graphics modules; the point is that it carries no `gbm` coupling, so
70// `shaders.rs` and the format code no longer pull in `gbm`.
71#[cfg(target_os = "android")]
72mod ahardwarebuffer_import;
73#[cfg(target_os = "linux")]
74mod fourcc;
75#[cfg(any(target_os = "macos", target_os = "ios"))]
76mod iosurface_import;
77#[cfg(target_os = "android")]
78mod native_fence;
79mod platform;
80mod processor;
81mod resources;
82mod shaders;
83mod shaders_common;
84// Engine GL tests, in three tiers (see crates/image/TESTING.md):
85//   portable                       — run on Linux AND macOS/ANGLE
86//   cfg(target_os = "linux")       — display probing, PBO/CUDA paths
87//   cfg(all(linux, dma_test_formats)) — DMA-BUF import/pool specifics
88// Per-item cfg gates inside the module select the tier; the mount
89// itself is unconditional so the macOS lane runs the portable tier.
90mod tests;
91mod threaded;
92
93#[cfg(target_os = "linux")]
94pub use context::probe_egl_displays;
95// These are accessed by sibling sub-modules via `super::context::` directly.
96// No re-export needed at the mod.rs level.
97pub use cache::{CacheStats, ConvertStats, GlCacheStats};
98pub use threaded::GLProcessorThreaded;
99
100/// Dynamically-loaded EGL 1.4 instance. The lifetime parameter is
101/// `'static` because the underlying `libloading::Library` is intentionally
102/// leaked at first load (see `EGL_LIB` in `context.rs` and the equivalent
103/// on macOS — drivers may retain internal state past explicit cleanup, so
104/// dlclose can SIGBUS on process exit).
105///
106/// Defined here at the `gl` module root so the `platform/` trait and both
107/// platform implementations can name it without dragging in a cross-cfg
108/// re-export. The Linux `context.rs` and the macOS `platform/macos.rs`
109/// both use this same alias.
110pub(super) type Egl = edgefirst_egl::Instance<
111    edgefirst_egl::Dynamic<&'static libloading::Library, edgefirst_egl::EGL1_4>,
112>;
113
114/// Identifies the type of EGL display used for headless OpenGL ES rendering.
115///
116/// The HAL creates a surfaceless GLES 3.0 context
117/// (`EGL_KHR_surfaceless_context` + `EGL_KHR_no_config_context`) and
118/// renders exclusively through FBOs backed by EGLImages imported from
119/// DMA-buf file descriptors. No window or PBuffer surface is created.
120///
121/// Displays are probed in priority order: PlatformDevice first (zero
122/// external dependencies), then GBM, then Default. Use
123/// [`probe_egl_displays`] to discover which are available and
124/// [`ImageProcessorConfig::egl_display`](crate::ImageProcessorConfig::egl_display)
125/// to override the auto-detection.
126///
127/// # Display Types
128///
129/// - **`PlatformDevice`** — Uses `EGL_EXT_device_enumeration` to query
130///   available EGL devices via `eglQueryDevicesEXT`, then selects the first
131///   device with `eglGetPlatformDisplay(EGL_EXT_platform_device, ...)`.
132///   Headless and compositor-free with zero external library dependencies.
133///   Works on NVIDIA GPUs and newer Vivante drivers.
134///
135/// - **`Gbm`** — Opens a DRM render node (e.g. `/dev/dri/renderD128`) and
136///   creates a GBM (Generic Buffer Manager) device, then calls
137///   `eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device)`. Requires
138///   `libgbm` and a DRM render node. Needed on ARM Mali (i.MX95) and older
139///   Vivante drivers that do not expose `EGL_EXT_platform_device`.
140///
141/// - **`Default`** — Calls `eglGetDisplay(EGL_DEFAULT_DISPLAY)`, letting the
142///   EGL implementation choose the display. On Wayland systems this connects
143///   to the compositor; on X11 it connects to the X server. May block on
144///   headless systems where a compositor is expected but not running.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
146pub enum EglDisplayKind {
147    Gbm,
148    PlatformDevice,
149    Default,
150}
151
152impl std::fmt::Display for EglDisplayKind {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            EglDisplayKind::Gbm => write!(f, "GBM"),
156            EglDisplayKind::PlatformDevice => write!(f, "PlatformDevice"),
157            EglDisplayKind::Default => write!(f, "Default"),
158        }
159    }
160}
161
162/// A validated, available EGL display discovered by [`probe_egl_displays`].
163#[derive(Debug, Clone)]
164pub struct EglDisplayInfo {
165    /// The type of EGL display.
166    pub kind: EglDisplayKind,
167    /// Human-readable description for logging/diagnostics
168    /// (e.g. "GBM via /dev/dri/renderD128").
169    pub description: String,
170}
171
172/// Tracks which data-transfer method is active for moving pixels
173/// between CPU memory and GPU textures/framebuffers.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub(crate) enum TransferBackend {
176    /// Zero-copy via EGLImage imported from DMA-buf file descriptors.
177    /// Available on i.MX8 (Vivante), i.MX95 (Mali), Jetson, and any
178    /// platform where `EGL_EXT_image_dma_buf_import` is present AND
179    /// the GPU can actually render through DMA-buf-backed textures.
180    DmaBuf,
181
182    /// Zero-copy via `EGL_ANGLE_iosurface_client_buffer` (macOS/iOS).
183    /// Available when ANGLE's Metal backend is loaded and the EGL
184    /// extension is advertised. The IOSurface is wrapped as an EGL
185    /// pbuffer and bound to a 2D texture via `eglBindTexImage`.
186    #[cfg(any(target_os = "macos", target_os = "ios"))]
187    IOSurface,
188
189    /// Zero-copy via `EGL_ANDROID_image_native_buffer` (Android). The
190    /// AHardwareBuffer is wrapped as an EGLImage via
191    /// `eglGetNativeClientBufferANDROID` → `eglCreateImageKHR` and bound
192    /// to a 2D texture via `glEGLImageTargetTexture2DOES` — persistent
193    /// bindings, like Linux DMA-BUF (unlike the per-pass IOSurface
194    /// pbuffer binds).
195    #[cfg(target_os = "android")]
196    AHardwareBuffer,
197
198    /// GPU buffer via Pixel Buffer Object. Used when DMA-buf is unavailable
199    /// but OpenGL is present. Data stays in GPU-accessible memory.
200    Pbo,
201
202    /// Synchronous `glTexSubImage2D` upload + `glReadPixels` readback.
203    /// Used when DMA-buf is unavailable or when the DMA-buf verification
204    /// probe fails (e.g. NVIDIA discrete GPUs where EGLImage creation
205    /// succeeds but rendered data is all zeros).
206    Sync,
207}
208
209impl TransferBackend {
210    /// Returns `true` if DMA-buf zero-copy is available.
211    pub(crate) fn is_dma(self) -> bool {
212        self == TransferBackend::DmaBuf
213    }
214
215    /// Returns `true` if the platform can import `TensorMemory::Dma`
216    /// tensors zero-copy: DMA-BUF EGLImages on Linux, IOSurface pbuffers
217    /// on macOS. Path-selection sites use this; probes that are
218    /// specifically about DMA-BUF semantics (e.g. the render-roundtrip
219    /// verification) keep `is_dma`.
220    pub(crate) fn is_zero_copy(self) -> bool {
221        #[cfg(any(target_os = "macos", target_os = "ios"))]
222        if self == TransferBackend::IOSurface {
223            return true;
224        }
225        #[cfg(target_os = "android")]
226        if self == TransferBackend::AHardwareBuffer {
227            return true;
228        }
229        self == TransferBackend::DmaBuf
230    }
231}
232
233/// Interpolation mode for int8 proto textures (GL_R8I cannot use GL_LINEAR).
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum Int8InterpolationMode {
236    /// texelFetch at nearest texel — simplest, fastest GPU execution.
237    Nearest,
238    /// texelFetch × 4 neighbors with shader-computed bilinear weights (default).
239    Bilinear,
240    /// Two-pass: dequant int8→f16 FBO, then existing f16 shader with GL_LINEAR.
241    TwoPass,
242}
243
244/// A rectangular region of interest expressed as normalised [0, 1] coordinates.
245#[derive(Debug, Clone, Copy)]
246pub(super) struct RegionOfInterest {
247    pub(super) left: f32,
248    pub(super) top: f32,
249    pub(super) right: f32,
250    pub(super) bottom: f32,
251}
252
253impl RegionOfInterest {
254    /// Build a source ROI from a pixel-space crop rectangle with a half-texel
255    /// inset. The inset ensures that `GL_LINEAR` filtering never samples
256    /// outside the crop boundary — at the extreme texture coordinates the
257    /// bilinear kernel is centred on the boundary texel and cannot reach
258    /// adjacent padding pixels.
259    ///
260    /// The result is clamped to [0, 1] so an out-of-bounds crop rectangle
261    /// cannot produce invalid texture coordinates.
262    ///
263    /// `crop`: pixel-space rectangle (left, top, width, height).
264    /// `tex_w`, `tex_h`: full texture dimensions in pixels.
265    pub(super) fn from_crop_clamped(crop: &crate::Rect, tex_w: usize, tex_h: usize) -> Self {
266        let half_x = 0.5 / tex_w as f32;
267        let half_y = 0.5 / tex_h as f32;
268        RegionOfInterest {
269            left: (crop.left as f32 / tex_w as f32 + half_x).clamp(0.0, 1.0),
270            top: ((crop.top + crop.height) as f32 / tex_h as f32 - half_y).clamp(0.0, 1.0),
271            right: ((crop.left + crop.width) as f32 / tex_w as f32 - half_x).clamp(0.0, 1.0),
272            bottom: (crop.top as f32 / tex_h as f32 + half_y).clamp(0.0, 1.0),
273        }
274    }
275}