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    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// CUDA test policy: turns a vacuous CUDA skip into a required-coverage
76// failure under HAL_TEST_REQUIRE_CUDA=1. Test-only -- every item in it is
77// consumed by `tests.rs` under #[cfg(test)] and by its own unit tests, so an
78// ungated module would be empty (and dead-code-flagged) in a release build.
79#[cfg(test)]
80mod cuda_policy;
81// `EGL_ANGLE_image_d3d11_texture` attribute assembly (pure); consumed by the
82// Windows leaf (`platform/windows.rs`) to build the EGLImage import target.
83#[cfg(target_os = "windows")]
84pub(super) mod d3d11_import;
85#[cfg(target_os = "linux")]
86mod fourcc;
87#[cfg(any(target_os = "macos", target_os = "ios"))]
88mod iosurface_import;
89#[cfg(target_os = "android")]
90mod native_fence;
91mod platform;
92mod processor;
93mod resources;
94mod shaders;
95mod shaders_common;
96// Engine GL tests, in three tiers (see crates/image/TESTING.md):
97//   portable                       — run on Linux AND macOS/ANGLE
98//   cfg(target_os = "linux")       — display probing, PBO/CUDA paths
99//   cfg(all(linux, dma_test_formats)) — DMA-BUF import/pool specifics
100// Per-item cfg gates inside the module select the tier; the mount
101// itself is unconditional so the macOS lane runs the portable tier.
102mod tests;
103mod threaded;
104
105#[cfg(target_os = "linux")]
106pub use context::probe_egl_displays;
107// These are accessed by sibling sub-modules via `super::context::` directly.
108// No re-export needed at the mod.rs level.
109pub use cache::{CacheStats, ConvertStats, GlCacheStats};
110pub use threaded::GLProcessorThreaded;
111
112/// Dynamically-loaded EGL 1.4 instance. The lifetime parameter is
113/// `'static` because the underlying `libloading::Library` is intentionally
114/// leaked at first load (see `EGL_LIB` in `context.rs` and the equivalent
115/// on macOS — drivers may retain internal state past explicit cleanup, so
116/// dlclose can SIGBUS on process exit).
117///
118/// Defined here at the `gl` module root so the `platform/` trait and both
119/// platform implementations can name it without dragging in a cross-cfg
120/// re-export. The Linux `context.rs` and the macOS `platform/macos.rs`
121/// both use this same alias.
122pub(super) type Egl = edgefirst_egl::Instance<
123    edgefirst_egl::Dynamic<&'static libloading::Library, edgefirst_egl::EGL1_4>,
124>;
125
126/// Handle returned by `GlPlatform::export_completion_fence` (and surfaced
127/// by `convert_with_fence`): a sync_file fd on Unix
128/// (`EGL_ANDROID_native_fence_sync`), an event NT handle on Windows, set
129/// when the D3D11 device fence reaches the value the convert queued.
130/// Keeps the engine free of `cfg` branches (precedent: `BlobFd` in
131/// `crates/tensor/src/blob.rs`).
132#[cfg(unix)]
133pub type CompletionFence = std::os::fd::OwnedFd;
134#[cfg(windows)]
135pub type CompletionFence = std::os::windows::io::OwnedHandle;
136
137/// Identifies the type of EGL display used for headless OpenGL ES rendering.
138///
139/// The HAL creates a surfaceless GLES 3.0 context
140/// (`EGL_KHR_surfaceless_context` + `EGL_KHR_no_config_context`) and
141/// renders exclusively through FBOs backed by EGLImages imported from
142/// DMA-buf file descriptors. No window or PBuffer surface is created.
143///
144/// Displays are probed in priority order: PlatformDevice first (zero
145/// external dependencies), then GBM, then Default. Use
146/// [`probe_egl_displays`] to discover which are available and
147/// [`ImageProcessorConfig::egl_display`](crate::ImageProcessorConfig::egl_display)
148/// to override the auto-detection.
149///
150/// # Display Types
151///
152/// - **`PlatformDevice`** — Uses `EGL_EXT_device_enumeration` to query
153///   available EGL devices via `eglQueryDevicesEXT`, then selects the first
154///   device with `eglGetPlatformDisplay(EGL_EXT_platform_device, ...)`.
155///   Headless and compositor-free with zero external library dependencies.
156///   Works on NVIDIA GPUs and newer Vivante drivers. Skipped on Qualcomm
157///   Adreno, whose device display crashes in `eglQueryString`; GBM is used
158///   there instead.
159///
160/// - **`Gbm`** — Opens a DRM render node (e.g. `/dev/dri/renderD128`) and
161///   creates a GBM (Generic Buffer Manager) device, then calls
162///   `eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm_device)`. Requires
163///   `libgbm` and a DRM render node. Needed on ARM Mali (i.MX95) and older
164///   Vivante drivers that do not expose `EGL_EXT_platform_device`.
165///
166/// - **`Default`** — Calls `eglGetDisplay(EGL_DEFAULT_DISPLAY)`, letting the
167///   EGL implementation choose the display. On Wayland systems this connects
168///   to the compositor; on X11 it connects to the X server. May block on
169///   headless systems where a compositor is expected but not running.
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
171pub enum EglDisplayKind {
172    Gbm,
173    PlatformDevice,
174    Default,
175}
176
177impl std::fmt::Display for EglDisplayKind {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        match self {
180            EglDisplayKind::Gbm => write!(f, "GBM"),
181            EglDisplayKind::PlatformDevice => write!(f, "PlatformDevice"),
182            EglDisplayKind::Default => write!(f, "Default"),
183        }
184    }
185}
186
187/// A validated, available EGL display discovered by [`probe_egl_displays`].
188#[derive(Debug, Clone)]
189pub struct EglDisplayInfo {
190    /// The type of EGL display.
191    pub kind: EglDisplayKind,
192    /// Human-readable description for logging/diagnostics
193    /// (e.g. "GBM via /dev/dri/renderD128").
194    pub description: String,
195}
196
197/// Tracks which data-transfer method is active for moving pixels
198/// between CPU memory and GPU textures/framebuffers.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub(crate) enum TransferBackend {
201    /// Zero-copy via EGLImage imported from DMA-buf file descriptors.
202    /// Available on i.MX8 (Vivante), i.MX95 (Mali), Jetson, and any
203    /// platform where `EGL_EXT_image_dma_buf_import` is present AND
204    /// the GPU can actually render through DMA-buf-backed textures.
205    DmaBuf,
206
207    /// Zero-copy via `EGL_ANGLE_iosurface_client_buffer` (macOS/iOS).
208    /// Available when ANGLE's Metal backend is loaded and the EGL
209    /// extension is advertised. The IOSurface is wrapped as an EGL
210    /// pbuffer and bound to a 2D texture via `eglBindTexImage`.
211    #[cfg(any(target_os = "macos", target_os = "ios"))]
212    IOSurface,
213
214    /// Zero-copy via `EGL_ANDROID_image_native_buffer` (Android). The
215    /// AHardwareBuffer is wrapped as an EGLImage via
216    /// `eglGetNativeClientBufferANDROID` → `eglCreateImageKHR` and bound
217    /// to a 2D texture via `glEGLImageTargetTexture2DOES` — persistent
218    /// bindings, like Linux DMA-BUF (unlike the per-pass IOSurface
219    /// pbuffer binds).
220    #[cfg(target_os = "android")]
221    AHardwareBuffer,
222
223    /// Zero-copy via `EGL_ANGLE_image_d3d11_texture` (Windows): the tensor's
224    /// `ID3D11Texture2D` wrapped as an EGLImage and bound with
225    /// `glEGLImageTargetTexture2DOES`. Unlike Linux and Android the binding
226    /// is re-issued per use, because ANGLE does not observe writes made to
227    /// the texture outside GL (see `AngleD3d11::PERSISTENT_TEX_BINDINGS`).
228    #[cfg(target_os = "windows")]
229    D3d11Texture,
230
231    /// GPU buffer via Pixel Buffer Object. Used when DMA-buf is unavailable
232    /// but OpenGL is present. Data stays in GPU-accessible memory. Windows
233    /// took this path until the D3D11 texture import landed and now reports
234    /// [`TransferBackend::D3d11Texture`]; PBO destinations still exist there
235    /// as `TensorMemory::Pbo` tensors.
236    Pbo,
237
238    /// Synchronous `glTexSubImage2D` upload + `glReadPixels` readback.
239    /// Used when DMA-buf is unavailable or when the DMA-buf verification
240    /// probe fails (e.g. NVIDIA discrete GPUs where EGLImage creation
241    /// succeeds but rendered data is all zeros).
242    Sync,
243}
244
245impl TransferBackend {
246    /// Returns `true` if DMA-buf zero-copy is available.
247    pub(crate) fn is_dma(self) -> bool {
248        self == TransferBackend::DmaBuf
249    }
250
251    /// Returns `true` if the platform can import `TensorMemory::DmaBuf`
252    /// tensors zero-copy: DMA-BUF EGLImages on Linux, IOSurface pbuffers
253    /// on macOS. Path-selection sites use this; probes that are
254    /// specifically about DMA-BUF semantics (e.g. the render-roundtrip
255    /// verification) keep `is_dma`.
256    pub(crate) fn is_zero_copy(self) -> bool {
257        #[cfg(any(target_os = "macos", target_os = "ios"))]
258        if self == TransferBackend::IOSurface {
259            return true;
260        }
261        #[cfg(target_os = "android")]
262        if self == TransferBackend::AHardwareBuffer {
263            return true;
264        }
265        #[cfg(target_os = "windows")]
266        if self == TransferBackend::D3d11Texture {
267            return true;
268        }
269        self == TransferBackend::DmaBuf
270    }
271}
272
273/// Interpolation mode for int8 proto textures (GL_R8I cannot use GL_LINEAR).
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum Int8InterpolationMode {
276    /// texelFetch at nearest texel — simplest, fastest GPU execution.
277    Nearest,
278    /// texelFetch × 4 neighbors with shader-computed bilinear weights (default).
279    Bilinear,
280    /// Two-pass: dequant int8→f16 FBO, then existing f16 shader with GL_LINEAR.
281    TwoPass,
282}
283
284/// A rectangular region of interest expressed as normalised [0, 1] coordinates.
285#[derive(Debug, Clone, Copy)]
286pub(super) struct RegionOfInterest {
287    pub(super) left: f32,
288    pub(super) top: f32,
289    pub(super) right: f32,
290    pub(super) bottom: f32,
291}
292
293impl RegionOfInterest {
294    /// Build a source ROI spanning a pixel-space crop rectangle's true edges,
295    /// clamped to [0, 1]. The `LINEAR` paths keep their kernel inside the crop
296    /// by clamping each sample to
297    /// [`render::sample_clamp_rect_within`](super::render::sample_clamp_rect_within)
298    /// of [`Self::uv_bounds`], not by insetting these coordinates: an inset
299    /// maps the crop's edge pixels to the edge texels' centres, which shifts
300    /// every sample of a 1:1 crop by up to half a texel and blurs it.
301    ///
302    /// `crop`: pixel-space rectangle (left, top, width, height).
303    /// `tex_w`, `tex_h`: full texture dimensions in pixels.
304    pub(super) fn from_crop(crop: &crate::Rect, tex_w: usize, tex_h: usize) -> Self {
305        RegionOfInterest {
306            left: (crop.left as f32 / tex_w as f32).clamp(0.0, 1.0),
307            top: ((crop.top + crop.height) as f32 / tex_h as f32).clamp(0.0, 1.0),
308            right: ((crop.left + crop.width) as f32 / tex_w as f32).clamp(0.0, 1.0),
309            bottom: (crop.top as f32 / tex_h as f32).clamp(0.0, 1.0),
310        }
311    }
312
313    /// `[u_min, v_min, u_max, v_max]` of this ROI, whichever way it is flipped.
314    pub(super) fn uv_bounds(&self) -> [f32; 4] {
315        [
316            self.left.min(self.right),
317            self.top.min(self.bottom),
318            self.left.max(self.right),
319            self.top.max(self.bottom),
320        ]
321    }
322}
323
324#[cfg(all(test, target_os = "windows"))]
325mod d3d11_backend_tests {
326    use super::TransferBackend;
327
328    #[test]
329    fn d3d11_texture_is_zero_copy_and_not_dma() {
330        assert!(TransferBackend::D3d11Texture.is_zero_copy());
331        assert!(!TransferBackend::D3d11Texture.is_dma());
332    }
333}