Files
ltk/src/gles_render/mod.rs
Pedro M. de Echanove Pasquin 806dee5167
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled
render, types, ci: resolution-time dp with per-canvas density, bounded GLES image cache, clippy gate, backend capability matrix
Length::dp no longer collapses to absolute pixels at construction: the design value travels in a new LengthBase::Dp variant and the density multiplication happens when the length is resolved. Previously dp( n ) baked in whatever density() returned at view-build time, so correctness across output changes depended on the view being rebuilt after set_density and in that order; now a density change is picked up by the very next paint with no reconstruction. Length::resolve keeps its signature (process density), and the new Length::resolve_with_density takes an explicit factor. dp becomes const in the bargain.
Density also becomes overridable per canvas, the first step towards surface-local responsive state. SoftwareCanvas and GlesCanvas carry a density: Option<f32> analogous to the layout_viewport introduced for sub-canvas fluid resolution: None means "use the process global", Canvas::set_density pins a local factor, and sub-canvases inherit it. All canvas-routed resolution honours it — geom_px / font_px for stock-widget design pixels, and the new Canvas::resolve_geom / resolve_font for explicit Length values, which every widget now uses in place of the raw l.resolve( canvas.viewport_layout(), EM ) pattern (row, column, wrap_grid, spacer, container, separator, button, text, rich_text, text_edit, list_item, vslider, image, and the container draw path). Overlay sizing keeps resolving against the main surface with the global density, which is what it describes. New tests cover explicit-density resolution, resolution-time application, the local-over-global override and sub-canvas inheritance.
The GLES image texture cache is now bounded. It was content-keyed but unbounded and never evicted, so a stream of distinct buffers — a photo carousel, video thumbnails — grew GPU memory for the lifetime of the canvas. The cache now tracks an estimated byte total (RGBA8, w × h × 4) against a 32 MiB budget and evicts least-recently-drawn textures on insert; the most recent entry is never evicted, so a single texture larger than the whole budget still draws and simply owns the cache until replaced. Drop-time cleanup is unchanged: drain deletes whatever the map holds.
CI gains a Clippy step (workspace, all targets, test-support, -D warnings) sharing the build cache of the test job, with make clippy mirroring the invocation locally and CONTRIBUTING listing it. Run make clippy locally before pushing the first time — the gate has not seen the tree yet and pre-existing lints will fail CI until addressed.
docs/backends.md formalises the software/GLES capability matrix that was previously scattered across per-method rustdoc: parity set (fills, strokes, text, images, paths, path clips), graceful degradations on software (flat-fill gradients, no shadows, no backdrop blur, hard bottom edge), GPU-only features (external textures), the shared Oklab-fallback limitation, and the cross-backend blit panic. Linked from README, onboarding and architecture's known-gaps list, which now states the parity gaps explicitly. The dp/density prose in architecture.md, lib.rs and the Length rustdoc is updated for resolution-time semantics and the per-canvas override.
2026-08-01 10:17:00 +02:00

468 lines
20 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.
//!
//! Rect 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 13
//! widgets so the union is barely larger than the sum. Arbitrary
//! path clipping ([`GlesCanvas::set_clip_path`]) captures the clipped
//! draws into an offscreen layer and composites it back through an
//! anti-aliased coverage mask, for a smooth clipped edge.
//!
//! # 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.
//! System-font resolution lives in `crate::system_fonts`.
//! * `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, VecDeque };
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 `system_fonts::default_handle`.
/// 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,
/// Layout-resolution viewport inherited from the parent canvas.
/// `None` on a root canvas (resolve against own size); sub-canvases
/// carry the root surface size so fluid geometry and fonts resolve
/// the same inside offscreen content (scroll viewports, clip layers)
/// as outside it.
pub( crate ) layout_viewport: Option<( f32, f32 )>,
/// Canvas-local pixel density for `Dp` resolution, when the owning
/// surface sits on an output whose density differs from the process
/// [`crate::density`]. `None` falls back to the global. Inherited by
/// sub-canvases.
pub( crate ) density: Option<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. Bounded by `image.rs`'s
// `IMAGE_CACHE_MAX_BYTES` with LRU eviction.
image_cache: HashMap<(u32, u32, u64), (glow::Texture, u32, u32)>,
// LRU order of `image_cache` keys, least-recent at the front.
image_cache_lru: VecDeque<(u32, u32, u64)>,
// Estimated GPU bytes held by `image_cache` (RGBA8: w × h × 4).
image_cache_bytes: usize,
// 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>,
/// Anti-aliased path clip (`set_clip_path`). While `clip_layer_active`,
/// `activate_target` redirects draws to `clip_layer` (a full-canvas
/// offscreen FBO); ending the clip composites that layer back onto `fbo`
/// multiplied by `clip_mask_tex` (an anti-aliased coverage texture covering
/// `clip_bbox`). The layer FBO is allocated lazily on the first path clip.
clip_layer: Option<( glow::Framebuffer, glow::Texture )>,
clip_layer_active: bool,
clip_mask_tex: Option<glow::Texture>,
clip_bbox: Rect,
/// Composite program (layer × coverage mask). Shared with sub-canvases.
clip_composite_program: glow::Program,
u_clip_mvp: glow::UniformLocation,
u_clip_layer: glow::UniformLocation,
u_clip_mask: glow::UniformLocation,
u_clip_canvas: glow::UniformLocation,
u_clip_bbox: glow::UniformLocation,
/// 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.clip_layer.take()
{
self.gl.delete_framebuffer( fbo );
self.gl.delete_texture( tex );
}
if let Some( tex ) = self.clip_mask_tex.take()
{
self.gl.delete_texture( 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 );
}
}
}
}