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