// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! GPU-accelerated rendering backend using EGL + GLES2 / GLES3. //! //! Mirrors the public surface of the software backend so that //! [`crate::core::Canvas`] can route widget draw calls to either backend //! by `match self`. The EGL context bootstrap lives in //! [`crate::egl_context`] — this module is just the renderer that runs //! once a context is current. //! //! Clipping is implemented with `glScissor`. When //! [`GlesCanvas::set_clip_rects`] receives multiple rects the //! bounding-box union is used as the scissor — coarse, but the //! partial-redraw path normally clusters the dirty rects of 1–3 //! widgets so the union is barely larger than the sum. Disjoint //! regions would want a stencil-buffer path; not implemented today. //! //! # Submodule layout //! //! * `setup` — `GlesCanvas::{new, sub_canvas, resize, set_font_registry, //! font_for, dpi/alpha accessors}`. //! * `framebuffer` — `GlesCanvas::{blit, present, activate_target, //! borrowed_texture, read_rgba_pixels, ensure_aux_*, snapshot_fbo_region, //! fill_backdrop, invalidate_aux}` — everything that manipulates the //! FBO or auxiliary snapshot textures. //! * `clip` — `GlesCanvas::{set_clip_rects, clear_clip, set_scissor, //! scissor_pixels, fill, clear, clear_rects_transparent}`. //! * `primitives` — `GlesCanvas::{fill_rect, fill_linear_gradient_rect, //! fill_radial_gradient_rect, fill_shadow_outer, fill_shadow_inset, //! stroke_rect, draw_line}`. //! * `text` — `GlesCanvas::{draw_text, measure_text}`. //! * `image` — `GlesCanvas::draw_image_data`. //! * `shaders` — GLSL ES 1.00 shader sources (const strings). //! * `helpers` — free functions: `ortho_rect`, `compile_program`, //! `alloc_fbo_tex`, `upload_*_texture`, handle extractors, //! `find_font` + `SYSTEM_FONT_CANDIDATES`. //! * `raii` — `FboBinding` / `ProgramBinding` scoped guards for the //! handful of operations that change global GL state for a scope //! and must guarantee restoration even on early return / panic //! (presently only `present`). The renderer otherwise relies on //! `activate_target`'s lazy re-bind at the entry of each draw //! method — see the module's own doc for when to use the guards //! and when not to. use std::collections::HashMap; use std::sync::Arc; use fontdue::Font; use glow::HasContext; use crate::theme::FontRegistry; use crate::types::Rect; pub( crate ) mod shaders; pub( crate ) mod helpers; pub( crate ) mod setup; pub( crate ) mod framebuffer; pub( crate ) mod clip; pub( crate ) mod primitives; pub( crate ) mod text; pub( crate ) mod image; mod raii; // ─── Public types ──────────────────────────────────────────────────────────── /// Which GLES profile the active context is. Stored so per-frame /// fast-paths can be selected without re-querying GL. #[ derive( Clone, Copy, PartialEq, Eq, Debug ) ] pub enum GlesVersion { V2, V3, } /// Borrowed view of the texture backing a [`GlesCanvas`]. /// /// The texture and framebuffer remain owned by ltk. Consumers may /// sample the texture while the canvas is alive, but must not delete /// or take ownership of the GL names. A resize can replace both /// names, so callers should query this after rendering/resizing, not /// cache it indefinitely. #[ derive( Clone, Copy, Debug ) ] pub struct BorrowedGlesTexture { /// Native GL texture name for the canvas color attachment. pub texture_id: u32, /// Native GL framebuffer name that owns `texture_id` as color attachment 0. pub framebuffer_id: u32, /// Glow texture handle for callers already using glow. pub texture: glow::Texture, /// Glow framebuffer handle for callers already using glow. pub framebuffer: glow::Framebuffer, /// Texture width in physical pixels. pub width: u32, /// Texture height in physical pixels. pub height: u32, /// The texture contains RGBA pixels with premultiplied alpha. pub premultiplied: bool, /// Whether consumers should treat the texture as vertically inverted. pub y_inverted: bool, } /// Cached glyph: rasterized bitmap placed in the shared atlas. pub ( super ) struct GlyphEntry { pub ( super ) metrics: fontdue::Metrics, pub ( super ) tex_w: i32, pub ( super ) tex_h: i32, pub ( super ) atlas_x: u32, pub ( super ) atlas_y: u32, } /// Cache key for the GLES glyph atlas — the GPU-side mirror of the /// software canvas's `GlyphKey`. `glyph_id` is the per-font index /// returned by HarfBuzz shaping; `size_bits` is `f32::to_bits` of /// `size * dpi_scale`; `font_id` is the address of the /// `Arc` used for rasterisation. pub( super ) type GlyphAtlasKey = ( u16, u32, usize ); pub( super ) const ATLAS_SIZE: u32 = 2048; // ─── GlesCanvas ────────────────────────────────────────────────────────────── /// GPU-accelerated canvas using EGL + GLES2/3. /// /// Renders into a persistent FBO (the "shadow canvas") so widget /// pixels survive across frames — this mirrors the software pixmap /// model and is what enables the partial-redraw path on the GPU side. /// `Self::present` blits the FBO onto the default framebuffer; the /// caller is responsible for the `eglSwapBuffers` that follows. pub struct GlesCanvas { pub gl: Arc, pub version: GlesVersion, /// Default font loaded from the system via `helpers::find_font`. /// Kept as a fallback for callers that do not route through the /// theme registry. pub font: Arc, /// Raw bytes of the default font. Required by rustybuzz for /// HarfBuzz shaping (see the `text_shaping` private module). Kept on the /// canvas so the shape pipeline has direct access without a /// global lookup. pub font_bytes: Arc>, /// TTC sub-face index for the default font (0 for non-`.ttc` files). pub font_face: u32, /// Optional theme font registry. Populated by the runtime after /// theme load; until then it is `None` and [`Self::font_for`] /// falls back to [`Self::font`]. pub font_registry: Option>, pub dpi_scale: f32, pub global_alpha: f32, pub width: u32, pub height: u32, // Shader programs. `Program` is a Copy handle; sub-canvases share // these with their parent (no reference counting needed since they // outlive the process). rect_program: glow::Program, tex_program: glow::Program, glyph_program: glow::Program, blit_program: glow::Program, sub_blit_program: glow::Program, linear_gradient_program: glow::Program, radial_gradient_program: glow::Program, shadow_outer_program: glow::Program, shadow_inset_program: glow::Program, // Shared geometry (a unit quad as two triangles) quad_vao: glow::VertexArray, _quad_vbo: glow::Buffer, // Uniform locations for rect shader u_rect_mvp: glow::UniformLocation, u_rect_color: glow::UniformLocation, u_rect_size: glow::UniformLocation, u_rect_radii: glow::UniformLocation, u_rect_stroke: glow::UniformLocation, u_rect_pad: glow::UniformLocation, // Uniform locations for texture shader u_tex_mvp: glow::UniformLocation, u_tex_opacity: glow::UniformLocation, u_tex_sampler: glow::UniformLocation, // Uniform locations for glyph shader u_glyph_mvp: glow::UniformLocation, u_glyph_color: glow::UniformLocation, u_glyph_opacity: glow::UniformLocation, u_glyph_sampler: glow::UniformLocation, u_glyph_uv_offset: glow::UniformLocation, u_glyph_uv_scale: glow::UniformLocation, glyph_batch_program: glow::Program, u_glyph_batch_color: glow::UniformLocation, u_glyph_batch_opacity: glow::UniformLocation, u_glyph_batch_sampler: glow::UniformLocation, pub ( super ) glyph_batch_vao: glow::VertexArray, pub ( super ) glyph_batch_vbo: glow::Buffer, // Uniform location for blit shader u_blit_sampler: glow::UniformLocation, // Uniform locations for sub-canvas blit shader u_subblit_mvp: glow::UniformLocation, u_subblit_sampler: glow::UniformLocation, u_subblit_opacity: glow::UniformLocation, u_subblit_fade_bottom: glow::UniformLocation, u_subblit_height_px: glow::UniformLocation, // Uniform locations for the linear gradient shader u_lingrad_mvp: glow::UniformLocation, u_lingrad_lut: glow::UniformLocation, u_lingrad_dir: glow::UniformLocation, u_lingrad_size: glow::UniformLocation, u_lingrad_line_length: glow::UniformLocation, u_lingrad_radii: glow::UniformLocation, u_lingrad_pad: glow::UniformLocation, u_lingrad_lut_domain_min: glow::UniformLocation, u_lingrad_lut_domain_span: glow::UniformLocation, // Uniform locations for the radial gradient shader u_radgrad_mvp: glow::UniformLocation, u_radgrad_lut: glow::UniformLocation, u_radgrad_center: glow::UniformLocation, u_radgrad_radius_frac: glow::UniformLocation, u_radgrad_size: glow::UniformLocation, u_radgrad_radii: glow::UniformLocation, u_radgrad_pad: glow::UniformLocation, u_radgrad_lut_domain_min: glow::UniformLocation, u_radgrad_lut_domain_span: glow::UniformLocation, // Uniform locations for the outer shadow shader u_shadow_mvp: glow::UniformLocation, u_shadow_size: glow::UniformLocation, u_shadow_padding: glow::UniformLocation, u_shadow_radii: glow::UniformLocation, u_shadow_spread: glow::UniformLocation, u_shadow_sigma: glow::UniformLocation, u_shadow_color: glow::UniformLocation, // Uniform locations for the inner (inset) shadow shader u_inset_mvp: glow::UniformLocation, u_inset_size: glow::UniformLocation, u_inset_padding: glow::UniformLocation, u_inset_radii: glow::UniformLocation, u_inset_spread: glow::UniformLocation, u_inset_sigma: glow::UniformLocation, u_inset_offset: glow::UniformLocation, u_inset_color: glow::UniformLocation, /// Inset-shadow shader variant for `BlendMode::Overlay`. Distinct /// from [`Self::shadow_inset_program`] because CSS Overlay cannot /// be expressed with fixed-function blend — this shader samples /// the just-snapshotted FBO content (via `aux_a`) and /// computes the per-channel Overlay formula in-shader, then /// outputs premultiplied. shadow_inset_overlay_program: glow::Program, u_inset_ov_mvp: glow::UniformLocation, u_inset_ov_size: glow::UniformLocation, u_inset_ov_padding: glow::UniformLocation, u_inset_ov_radii: glow::UniformLocation, u_inset_ov_spread: glow::UniformLocation, u_inset_ov_sigma: glow::UniformLocation, u_inset_ov_offset: glow::UniformLocation, u_inset_ov_color: glow::UniformLocation, u_inset_ov_snapshot: glow::UniformLocation, u_inset_ov_canvas_size: glow::UniformLocation, /// Horizontal pass of the separable Gaussian used by /// [`Self::fill_backdrop`](framebuffer). Samples /// `aux_a` (snapshot of the main FBO) and writes the /// horizontally-blurred result into `aux_b`. backdrop_blur_h_program: glow::Program, u_bd_h_source: glow::UniformLocation, u_bd_h_texel: glow::UniformLocation, u_bd_h_canvas_size: glow::UniformLocation, u_bd_h_sigma: glow::UniformLocation, /// Vertical pass of the separable Gaussian combined with the SDF /// clip to the surface shape and optional tint. Reads /// `aux_b` (H-blurred) and writes to the main FBO at the /// surface rect. backdrop_composite_program: glow::Program, u_bd_c_mvp: glow::UniformLocation, u_bd_c_source: glow::UniformLocation, u_bd_c_canvas_size: glow::UniformLocation, u_bd_c_texel: glow::UniformLocation, u_bd_c_sigma: glow::UniformLocation, u_bd_c_size: glow::UniformLocation, u_bd_c_padding: glow::UniformLocation, u_bd_c_radii: glow::UniformLocation, u_bd_c_tint: glow::UniformLocation, /// Fast (low-quality) horizontal Gaussian. Same role as /// `backdrop_blur_h_program` but with a 9-tap kernel /// (`RADIUS = 4`) instead of 41 taps. Used during animations / /// drags via [`crate::render::low_quality_paint`]. backdrop_fast_blur_h_program: glow::Program, u_bd_fh_source: glow::UniformLocation, u_bd_fh_texel: glow::UniformLocation, u_bd_fh_canvas_size: glow::UniformLocation, u_bd_fh_sigma: glow::UniformLocation, /// Fast (low-quality) vertical + SDF + tint composite. 9-tap /// kernel counterpart to `backdrop_composite_program`. backdrop_fast_composite_program: glow::Program, u_bd_fc_mvp: glow::UniformLocation, u_bd_fc_source: glow::UniformLocation, u_bd_fc_canvas_size: glow::UniformLocation, u_bd_fc_texel: glow::UniformLocation, u_bd_fc_sigma: glow::UniformLocation, u_bd_fc_size: glow::UniformLocation, u_bd_fc_padding: glow::UniformLocation, u_bd_fc_radii: glow::UniformLocation, u_bd_fc_tint: glow::UniformLocation, atlas_texture: glow::Texture, /// Upload format of `atlas_texture`. On GLES3 we use the modern /// single-channel `GL_RED` over `GL_R8`; on GLES2 we keep the /// legacy `GL_LUMINANCE` because `GL_R8` / `GL_RED` did not exist /// before ES3. The fragment shader samples `.r` and is identical /// for both — `GL_LUMINANCE` replicates the single channel into /// `.r=.g=.b`, so `.r` carries the coverage either way. pub ( super ) atlas_format: u32, atlas_cursor_x: u32, atlas_cursor_y: u32, atlas_row_height: u32, // (glyph_id, size_key, font_id) → GlyphEntry. `glyph_id` is the // per-font glyph index returned by HarfBuzz shaping; the cache // therefore persists Arabic / Devanagari / CJK shaped forms // independently of the source codepoints. glyph_cache: HashMap, // Reusable texture cache for images. Keyed by // `(width, height, content fingerprint)` rather than the source // buffer's heap address — pointer-keying produced ghosting when // short-lived `Arc>` buffers got dropped and the // allocator handed the same address to a different buffer next // frame (the cache would happily serve the stale texture). // Content-keying tolerates that case at the cost of one // `DefaultHasher` pass over the bytes per draw call — fast for // any reasonable icon size. image_cache: HashMap<(u32, u32, u64), (glow::Texture, u32, u32)>, // Gradient LUT cache: FNV-ish hash of the 512×RGBA8 LUT bytes → texture. // Gradients are theme-derived and constant across frames; caching avoids // a glTexImage2D round-trip (create + upload + delete) on every draw call. // Cleared via `clear_gradient_cache()` on theme changes. gradient_lut_cache: HashMap, /// Current scissor: `Some(rect)` when a clip is installed /// (GL_SCISSOR_TEST is enabled), `None` when cleared. clip_scissor: Option, /// Persistent shadow framebuffer. All draw methods bind this; /// [`Self::present`](framebuffer) is the only call that switches /// to the default framebuffer. fbo: glow::Framebuffer, /// Color attachment for `fbo`. Reallocated on resize. fbo_tex: glow::Texture, /// Auxiliary FBO + texture used as a snapshot of `fbo` /// for framebuffer-fetch-style effects (CSS `Overlay` blend, /// backdrop blur). Lazily allocated on first use; dropped on /// resize so the next user re-allocates at the new size. aux_a: Option<( glow::Framebuffer, glow::Texture )>, /// Second auxiliary FBO, used as ping-pong target for the /// separable Gaussian blur in backdrop compositing. Uses LINEAR /// filtering for the V-pass bilinear sampling (vs `aux_a`'s /// NEAREST). aux_b: Option<( glow::Framebuffer, glow::Texture )>, } // ─── Drop ──────────────────────────────────────────────────────────────────── /// Free this canvas's owned GL resources: FBO, color attachment, aux /// FBOs if allocated, and any cached glyph / image textures. Shader /// programs and the quad VAO/VBO are shared with sub-canvases and /// intentionally NOT deleted here — they leak at process exit, which /// is fine for a process-wide GL context. impl Drop for GlesCanvas { fn drop( &mut self ) { // SAFETY: every handle freed below was created through `self.gl` — // either in `setup.rs::new` / `sub_canvas` (`fbo`, `fbo_tex`), // `framebuffer.rs::ensure_aux_a` / `ensure_aux_b` (`aux_a`, `aux_b`), // `text.rs::draw_text` (`glyph_cache`), `image.rs::draw_image_data` // (`image_cache`), or `primitives.rs::ensure_lut_texture` // (`gradient_lut_cache`). Each container `drain` / `take` is // consumed once so no double-free is possible. Caller must keep // the GL context current at drop time — this is documented on // `core::UiSurface::from_current_gles_loader` and // `from_canvas_with_egl_context`. unsafe { self.gl.delete_framebuffer( self.fbo ); self.gl.delete_texture( self.fbo_tex ); if let Some( ( fbo, tex ) ) = self.aux_a.take() { self.gl.delete_framebuffer( fbo ); self.gl.delete_texture( tex ); } if let Some( ( fbo, tex ) ) = self.aux_b.take() { self.gl.delete_framebuffer( fbo ); self.gl.delete_texture( tex ); } self.glyph_cache.clear(); self.gl.delete_texture( self.atlas_texture ); for ( _, ( tex, _, _ ) ) in self.image_cache.drain() { self.gl.delete_texture( tex ); } for ( _, tex ) in self.gradient_lut_cache.drain() { self.gl.delete_texture( tex ); } } } }