edgefirst_image/gl/mod.rs
1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4#![cfg(any(target_os = "linux", target_os = "macos"))]
5#![cfg(feature = "opengl")]
6// Several types defined at the `gl` module root (EglDisplayKind,
7// TransferBackend, RegionOfInterest, etc.) are consumed only by the
8// Linux-only inner modules (`context`, `processor`, ...). The macOS
9// path uses its own `MacosGlProcessor` + `iosurface_import` modules and
10// does not touch every shared type, so some appear unused on macOS.
11// Rather than fragmenting the type definitions per platform, suppress
12// the dead-code lint on non-Linux targets.
13#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
14
15// Module layout:
16// - `platform/` — cross-platform display/EGL-loader seam (both OSes)
17// - Linux-only: `context`, `processor`, `threaded`, `dma_import`,
18// `cache`, `resources`, `shaders`, `tests`
19// - macOS-only: `iosurface_import`, `macos_processor`
20// The macOS processor is parallel to (not a refactor of) the Linux
21// threaded processor — see `crates/image/ARCHITECTURE.md` for the
22// rationale and the planned convergence story.
23
24#[cfg(target_os = "linux")]
25macro_rules! function {
26 () => {{
27 fn f() {}
28 fn type_name_of<T>(_: T) -> &'static str {
29 std::any::type_name::<T>()
30 }
31 let name = type_name_of(f);
32
33 // Find and cut the rest of the path
34 match &name[..name.len() - 3].rfind(':') {
35 Some(pos) => &name[pos + 1..name.len() - 3],
36 None => &name[..name.len() - 3],
37 }
38 }};
39}
40
41#[cfg(target_os = "linux")]
42mod cache;
43#[cfg(target_os = "linux")]
44mod context;
45#[cfg(target_os = "linux")]
46mod dma_import;
47#[cfg(target_os = "macos")]
48mod iosurface_import;
49#[cfg(target_os = "macos")]
50mod macos_processor;
51mod platform;
52#[cfg(target_os = "linux")]
53mod processor;
54#[cfg(target_os = "linux")]
55mod resources;
56#[cfg(target_os = "linux")]
57mod shaders;
58#[cfg(target_os = "linux")]
59mod tests;
60#[cfg(target_os = "linux")]
61mod threaded;
62
63#[cfg(target_os = "linux")]
64pub use context::probe_egl_displays;
65// These are accessed by sibling sub-modules via `super::context::` directly.
66// No re-export needed at the mod.rs level.
67#[cfg(target_os = "macos")]
68pub use macos_processor::MacosGlProcessor;
69#[cfg(target_os = "linux")]
70pub use threaded::GLProcessorThreaded;
71
72/// Dynamically-loaded EGL 1.4 instance. The lifetime parameter is
73/// `'static` because the underlying `libloading::Library` is intentionally
74/// leaked at first load (see `EGL_LIB` in `context.rs` and the equivalent
75/// on macOS — drivers may retain internal state past explicit cleanup, so
76/// dlclose can SIGBUS on process exit).
77///
78/// Defined here at the `gl` module root so the `platform/` trait and both
79/// platform implementations can name it without dragging in a cross-cfg
80/// re-export. The Linux `context.rs` and the macOS `platform/macos.rs`
81/// both use this same alias.
82pub(super) type Egl =
83 khronos_egl::Instance<khronos_egl::Dynamic<&'static libloading::Library, khronos_egl::EGL1_4>>;
84
85/// Identifies the type of EGL display used for headless OpenGL ES rendering.
86///
87/// The HAL creates a surfaceless GLES 3.0 context
88/// (`EGL_KHR_surfaceless_context` + `EGL_KHR_no_config_context`) and
89/// renders exclusively through FBOs backed by EGLImages imported from
90/// DMA-buf file descriptors. No window or PBuffer surface is created.
91///
92/// Displays are probed in priority order: PlatformDevice first (zero
93/// external dependencies), then GBM, then Default. Use
94/// [`probe_egl_displays`] to discover which are available and
95/// [`ImageProcessorConfig::egl_display`](crate::ImageProcessorConfig::egl_display)
96/// to override the auto-detection.
97///
98/// # Display Types
99///
100/// - **`PlatformDevice`** — Uses `EGL_EXT_device_enumeration` to query
101/// available EGL devices via `eglQueryDevicesEXT`, then selects the first
102/// device with `eglGetPlatformDisplay(EGL_EXT_platform_device, ...)`.
103/// Headless and compositor-free with zero external library dependencies.
104/// Works on NVIDIA GPUs and newer Vivante drivers.
105///
106/// - **`Gbm`** — Opens a DRM render node (e.g. `/dev/dri/renderD128`) and
107/// creates a GBM (Generic Buffer Manager) device, then calls
108/// `eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device)`. Requires
109/// `libgbm` and a DRM render node. Needed on ARM Mali (i.MX95) and older
110/// Vivante drivers that do not expose `EGL_EXT_platform_device`.
111///
112/// - **`Default`** — Calls `eglGetDisplay(EGL_DEFAULT_DISPLAY)`, letting the
113/// EGL implementation choose the display. On Wayland systems this connects
114/// to the compositor; on X11 it connects to the X server. May block on
115/// headless systems where a compositor is expected but not running.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117pub enum EglDisplayKind {
118 Gbm,
119 PlatformDevice,
120 Default,
121}
122
123impl std::fmt::Display for EglDisplayKind {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 match self {
126 EglDisplayKind::Gbm => write!(f, "GBM"),
127 EglDisplayKind::PlatformDevice => write!(f, "PlatformDevice"),
128 EglDisplayKind::Default => write!(f, "Default"),
129 }
130 }
131}
132
133/// A validated, available EGL display discovered by [`probe_egl_displays`].
134#[derive(Debug, Clone)]
135pub struct EglDisplayInfo {
136 /// The type of EGL display.
137 pub kind: EglDisplayKind,
138 /// Human-readable description for logging/diagnostics
139 /// (e.g. "GBM via /dev/dri/renderD128").
140 pub description: String,
141}
142
143/// Tracks which data-transfer method is active for moving pixels
144/// between CPU memory and GPU textures/framebuffers.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub(crate) enum TransferBackend {
147 /// Zero-copy via EGLImage imported from DMA-buf file descriptors.
148 /// Available on i.MX8 (Vivante), i.MX95 (Mali), Jetson, and any
149 /// platform where `EGL_EXT_image_dma_buf_import` is present AND
150 /// the GPU can actually render through DMA-buf-backed textures.
151 DmaBuf,
152
153 /// Zero-copy via `EGL_ANGLE_iosurface_client_buffer` (macOS).
154 /// Available when ANGLE's Metal backend is loaded and the EGL
155 /// extension is advertised. The IOSurface is wrapped as an EGL
156 /// pbuffer and bound to a 2D texture via `eglBindTexImage`.
157 #[cfg(target_os = "macos")]
158 IOSurface,
159
160 /// GPU buffer via Pixel Buffer Object. Used when DMA-buf is unavailable
161 /// but OpenGL is present. Data stays in GPU-accessible memory.
162 Pbo,
163
164 /// Synchronous `glTexSubImage2D` upload + `glReadnPixels` readback.
165 /// Used when DMA-buf is unavailable or when the DMA-buf verification
166 /// probe fails (e.g. NVIDIA discrete GPUs where EGLImage creation
167 /// succeeds but rendered data is all zeros).
168 Sync,
169}
170
171impl TransferBackend {
172 /// Returns `true` if DMA-buf zero-copy is available.
173 pub(crate) fn is_dma(self) -> bool {
174 self == TransferBackend::DmaBuf
175 }
176}
177
178/// Interpolation mode for int8 proto textures (GL_R8I cannot use GL_LINEAR).
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub enum Int8InterpolationMode {
181 /// texelFetch at nearest texel — simplest, fastest GPU execution.
182 Nearest,
183 /// texelFetch × 4 neighbors with shader-computed bilinear weights (default).
184 Bilinear,
185 /// Two-pass: dequant int8→f16 FBO, then existing f16 shader with GL_LINEAR.
186 TwoPass,
187}
188
189/// A rectangular region of interest expressed as normalised [0, 1] coordinates.
190#[derive(Debug, Clone, Copy)]
191pub(super) struct RegionOfInterest {
192 pub(super) left: f32,
193 pub(super) top: f32,
194 pub(super) right: f32,
195 pub(super) bottom: f32,
196}
197
198impl RegionOfInterest {
199 /// Build a source ROI from a pixel-space crop rectangle with a half-texel
200 /// inset. The inset ensures that `GL_LINEAR` filtering never samples
201 /// outside the crop boundary — at the extreme texture coordinates the
202 /// bilinear kernel is centred on the boundary texel and cannot reach
203 /// adjacent padding pixels.
204 ///
205 /// The result is clamped to [0, 1] so an out-of-bounds crop rectangle
206 /// cannot produce invalid texture coordinates.
207 ///
208 /// `crop`: pixel-space rectangle (left, top, width, height).
209 /// `tex_w`, `tex_h`: full texture dimensions in pixels.
210 pub(super) fn from_crop_clamped(crop: &crate::Rect, tex_w: usize, tex_h: usize) -> Self {
211 let half_x = 0.5 / tex_w as f32;
212 let half_y = 0.5 / tex_h as f32;
213 RegionOfInterest {
214 left: (crop.left as f32 / tex_w as f32 + half_x).clamp(0.0, 1.0),
215 top: ((crop.top + crop.height) as f32 / tex_h as f32 - half_y).clamp(0.0, 1.0),
216 right: ((crop.left + crop.width) as f32 / tex_w as f32 - half_x).clamp(0.0, 1.0),
217 bottom: (crop.top as f32 / tex_h as f32 + half_y).clamp(0.0, 1.0),
218 }
219 }
220}