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.
This commit is contained in:
@@ -15,6 +15,14 @@
|
||||
//! serve the stale texture for the new content. Content-keying makes
|
||||
//! that impossible: identical bytes → identical key, regardless of
|
||||
//! where they live in memory.
|
||||
//!
|
||||
//! The cache is bounded to [`IMAGE_CACHE_MAX_BYTES`] of estimated GPU
|
||||
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
|
||||
//! eviction, so a stream of distinct buffers (a photo carousel, video
|
||||
//! thumbnails) recycles textures instead of growing GPU memory for the
|
||||
//! canvas' lifetime. The most recent entry is never evicted, so a
|
||||
//! single texture larger than the whole budget still draws — the cache
|
||||
//! then holds that one entry until something replaces it.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{ Hash, Hasher };
|
||||
@@ -26,6 +34,12 @@ use crate::types::Rect;
|
||||
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
||||
use super::GlesCanvas;
|
||||
|
||||
/// Byte budget for the image texture cache. 32 MiB holds a phone-sized
|
||||
/// wallpaper (~10 MB at 1080×2400) plus a working set of icons and
|
||||
/// thumbnails; sized for mobile GPUs where this memory competes with
|
||||
/// the compositor.
|
||||
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||
|
||||
/// Compute a 64-bit fingerprint of an RGBA buffer for the texture
|
||||
/// cache. Hashes the full byte slice for small buffers (icons,
|
||||
/// thumbnails — below 16 KB ≈ 64×64 RGBA), and falls back to a
|
||||
@@ -85,6 +99,13 @@ impl GlesCanvas
|
||||
{
|
||||
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
||||
self.image_cache.insert( cache_key, ( tex, img_w, img_h ) );
|
||||
self.image_cache_bytes += ( img_w as usize ) * ( img_h as usize ) * 4;
|
||||
self.image_cache_lru.push_back( cache_key );
|
||||
self.evict_image_cache_overflow();
|
||||
} else if let Some( pos ) = self.image_cache_lru.iter().position( |k| *k == cache_key )
|
||||
{
|
||||
self.image_cache_lru.remove( pos );
|
||||
self.image_cache_lru.push_back( cache_key );
|
||||
}
|
||||
|
||||
// Snap to integer pixels. With GL_LINEAR sampling, a
|
||||
@@ -107,9 +128,9 @@ impl GlesCanvas
|
||||
let mvp = ortho_rect( self.width, self.height, dest );
|
||||
let alpha = opacity * self.global_alpha;
|
||||
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
||||
// `self.image_cache` so it outlives the call. The image cache
|
||||
// stays valid as long as `&mut self` is held — no eviction
|
||||
// path runs concurrently with the draw.
|
||||
// `self.image_cache` so it outlives the call. Eviction ran
|
||||
// before this borrow and never removes the most-recent key,
|
||||
// which is `cache_key`.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.tex_program ) );
|
||||
@@ -126,6 +147,27 @@ impl GlesCanvas
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete least-recently-drawn cached textures until the estimated
|
||||
/// byte total fits [`IMAGE_CACHE_MAX_BYTES`]. The back of the LRU —
|
||||
/// the entry the current draw is about to use — is never removed.
|
||||
fn evict_image_cache_overflow( &mut self )
|
||||
{
|
||||
while self.image_cache_bytes > IMAGE_CACHE_MAX_BYTES && self.image_cache_lru.len() > 1
|
||||
{
|
||||
let Some( key ) = self.image_cache_lru.pop_front() else { break };
|
||||
if let Some( ( tex, w, h ) ) = self.image_cache.remove( &key )
|
||||
{
|
||||
self.image_cache_bytes = self.image_cache_bytes
|
||||
.saturating_sub( ( w as usize ) * ( h as usize ) * 4 );
|
||||
// SAFETY: `tex` was created through `self.gl` in the insert
|
||||
// path above and just left the map, so it is deleted exactly
|
||||
// once. Deleting a bound texture is defined in GLES (the
|
||||
// binding reverts to 0); no draw is in flight here.
|
||||
unsafe { self.gl.delete_texture( tex ); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw an externally-owned GL texture into `dest`.
|
||||
///
|
||||
/// The caller owns the texture and is responsible for keeping it valid
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
//! method — see the module's own doc for when to use the guards
|
||||
//! and when not to.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{ HashMap, VecDeque };
|
||||
use std::sync::Arc;
|
||||
|
||||
use fontdue::Font;
|
||||
@@ -157,6 +157,11 @@ pub struct GlesCanvas
|
||||
/// 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,
|
||||
@@ -349,8 +354,13 @@ pub struct GlesCanvas
|
||||
// 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.
|
||||
// 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
|
||||
|
||||
@@ -337,6 +337,7 @@ impl GlesCanvas
|
||||
font_registry: None,
|
||||
dpi_scale: 1.0,
|
||||
layout_viewport: None,
|
||||
density: None,
|
||||
global_alpha: 1.0,
|
||||
width,
|
||||
height,
|
||||
@@ -459,6 +460,8 @@ impl GlesCanvas
|
||||
atlas_row_height: 0,
|
||||
glyph_cache: HashMap::new(),
|
||||
image_cache: HashMap::new(),
|
||||
image_cache_lru: std::collections::VecDeque::new(),
|
||||
image_cache_bytes: 0,
|
||||
gradient_lut_cache: HashMap::new(),
|
||||
clip_scissor: None,
|
||||
clip_layer: None,
|
||||
@@ -550,6 +553,7 @@ impl GlesCanvas
|
||||
dpi_scale: self.dpi_scale,
|
||||
layout_viewport: Some( self.layout_viewport.unwrap_or(
|
||||
( self.width as f32, self.height as f32 ) ) ),
|
||||
density: self.density,
|
||||
global_alpha: self.global_alpha,
|
||||
width,
|
||||
height,
|
||||
@@ -672,6 +676,8 @@ impl GlesCanvas
|
||||
atlas_row_height: 0,
|
||||
glyph_cache: HashMap::new(),
|
||||
image_cache: HashMap::new(),
|
||||
image_cache_lru: std::collections::VecDeque::new(),
|
||||
image_cache_bytes: 0,
|
||||
gradient_lut_cache: HashMap::new(),
|
||||
clip_scissor: None,
|
||||
clip_layer: None,
|
||||
|
||||
Reference in New Issue
Block a user