`cargo doc --no-deps` was emitting eight warnings about intra-doc links resolving to private items or ambiguous fn/module names. None reflect a behaviour bug; they were just noise that cluttered the docs build output. Five of the warnings came from doc comments that cross-referenced items not in the rendered public API. `widget::scroll::Scroll` (struct), `Scroll::horizontal`, `Scroll::both`, `event_loop::text_editing` (module) and `text_shaping` (module) are all `pub` in their own modules, but the `widget` and `event_loop` parents are private to the crate root, so rustdoc treats them as private when resolving links from items that ARE on the public surface (`ScrollAxis`, the `scroll` constructor, `font_bytes`, the `text_edit` module docs). The fix is to drop the link syntax for those references: keep the identifier in backticks (so it still renders as code) but remove the surrounding `[...]` so rustdoc doesn't try to resolve it. Where the cross-reference had no semantic load beyond "see this module", the prose is rephrased to name the module without trying to link to it (e.g. "the `event_loop::text_editing` private module", "see the `text_shaping` private module"). The remaining three warnings were `palette` / `surface` linking ambiguously between a module of that name and a function of the same name within `crate::theme`. Adding `()` after the identifier inside the brackets (`palette` → `palette()`, `surface` → `surface()`) disambiguates to the function, which is the intent in all three sites — the surrounding text talks about "per-slot shorthand accessors", which is what the functions are. After this `cargo doc --no-deps` runs clean with no warnings.
424 lines
17 KiB
Rust
424 lines
17 KiB
Rust
// SPDX-License-Identifier: LGPL-2.1-only
|
||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||
|
||
//! 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<fontdue::Font>` 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<glow::Context>,
|
||
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<Font>,
|
||
/// 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<Vec<u8>>,
|
||
/// 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<Arc<FontRegistry>>,
|
||
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<GlyphAtlasKey, GlyphEntry>,
|
||
|
||
// 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<Vec<u8>>` 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<u64, glow::Texture>,
|
||
|
||
/// Current scissor: `Some(rect)` when a clip is installed
|
||
/// (GL_SCISSOR_TEST is enabled), `None` when cleared.
|
||
clip_scissor: Option<Rect>,
|
||
|
||
/// 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 );
|
||
}
|
||
}
|
||
}
|
||
}
|