Keep the GLES path when the scale arrives first, stop re-rendering what does not change, and let the compositor skip what the surface covers
Measured on a Librem 5 (Cortex-A53, GC7000Lite, 720×1440 at scale 2) with a frame trace on both sides of the socket: the shell's main surface repainted in 110 to 136 ms, quick settings drew at 20 fps with the GPU saturated, and every panel open uploaded its wallpaper and icons again. Most of it was the toolkit doing work the frame did not need. Backend choice. When the compositor announces the output scale before the first `configure`, `scale_factor_changed` allocated an SHM pool for the surface and `on_configure` took that pool as the sign that the software path had been chosen, locking the surface to tiny-skia for its whole life. Every ltk main surface on a scale-2 phone ran in software while its overlays, which inherit the scale from the main and never receive that event, ran on the GPU. The scale handler now only reallocates a pool that already exists; before the first configure neither path is chosen. Wallpapers. `WallpaperBundle::for_size` returned the crop at the source resolution, a 1103×2160 slice of a 4K image for a 720×1440 panel, 10 MB uploaded per texture and a hundred milliseconds each time. The crop is now downscaled to the surface (bilinear, once per size, cached), also for landscape surfaces smaller than the source. Viewport. `Element::Viewport` created a fresh sub-canvas every frame, on GLES a framebuffer, a 2048² glyph atlas and empty image and glyph caches, so a panel clipped to a height that animates re-uploaded its wallpaper and re-rasterised every glyph on every frame. Without a bottom fade the clip is now a scissor on the parent canvas (a mask on the software backend): no offscreen pass, no blit, and the child's textures stay in the parent's caches. `local_viewport` is honoured through a temporary override of the layout viewport. Viewports with a fade keep their sub-canvas, which, like the scroll sub-canvases, is now kept across frames and resized in place instead of reallocated when its size changes. Caches. The image texture cache moves from the canvas to the GL context: every canvas and sub-canvas shares one content-keyed LRU (32 MiB), so an overlay or subsurface that is destroyed and recreated finds its images resident. A GLES canvas dropped with an overlay is pooled in `AppData::canvas_pool` and handed to the next overlay, with its compiled programs and glyph atlas, which took 130 to 290 ms to build on that GPU. Subsurface slots keep their scroll and viewport sub-canvases between rasters. Surfaces. A `container.surface()` with shadows costs a Gaussian drop-shadow pass, a fill and one pass per inset shadow, all full-quad fragment shaders; a dozen glass cards were 40 ms of GPU per frame for chrome that never changes. `Canvas::fill_surface` on GLES now renders the chrome once per size, corners and style into a texture held by the context cache (`gles_render/surface_cache.rs`, 16 MiB LRU) and blits it, drawing directly only for surfaces with `PlusLighter` or `Overlay` insets, which depend on the backdrop. Opaque regions. ltk never called `wl_surface.set_opaque_region`, so the compositor blended every ltk surface and repainted everything underneath a full-screen homescreen. A surface whose `background_color` is opaque now declares itself opaque, and the new `App::overlay_opaque_region( id )` lets an overlay declare the physical rects it paints opaque, such as a panel column over an opaque wallpaper. `sync_opaque_region` sends the rects only when they change. Frame pacing. `eglSwapBuffers` blocked 13 to 37 ms per frame waiting for Mesa's own throttle callback; ltk paces itself on `wl_surface.frame`, so `make_current` sets the swap interval to 0. A subsurface move is now applied only when its parent has no frame callback outstanding, and the parent commit that applies it requests the next callback: a drag commits once per vblank with the latest finger position instead of once per input sample, without the app throttling samples itself. While the app reports `subsurface_motion_only`, the main's callback, and the callback of an overlay that hosts a subsurface, no longer force a full repaint; they mark the surface and the reconcile keeps the cadence with a bare commit when nothing moved. Swipe releases arm a settle the same way instead of dirtying every cache. Two hooks, `on_overlay_frame_committed` and `on_subsurface_committed`, let an app hand a panel over between its host and a sliding subsurface on real commits rather than loop ticks. Lines. A diagonal `draw_line` on GLES stamped one `fill_rect` per pixel of length, 190 draw calls for a spinner and over 2000 for a pattern-lock stroke; it is now a single rotated quad through the rect shader, with round caps and antialiased edges. A pixel test covers the scissor viewport, `for_size` gets a downscale test, and `docs/architecture.md`, `docs/widgets.md` and `docs/backends.md` describe the new hook, the viewport paths, the shared caches and the line rasteriser.
This commit is contained in:
@@ -83,7 +83,8 @@ In practice, that model is easiest to adopt in three steps:
|
||||
|
||||
- `shell_mode()` → `ShellMode::Layer( Layer::Background | Bottom | Top | Overlay )`.
|
||||
- `layer_anchor()`, `layer_size()`, `exclusive_zone()`, `keyboard_exclusive()` — the layer-shell knobs.
|
||||
- `background_color()` → `Color::rgba( 0, 0, 0, 0 )` for transparent surfaces (panels, OSDs).
|
||||
- `background_color()` → `Color::rgba( 0, 0, 0, 0 )` for transparent surfaces (panels, OSDs). An opaque colour also declares the whole surface opaque to the compositor, which then neither blends it nor repaints what lies underneath — return one whenever the view covers the surface (a wallpaper-backed homescreen).
|
||||
- `overlay_opaque_region( id )` → the physical rects of an overlay that are fully opaque this frame (a panel column painted over an opaque wallpaper), for the same compositor savings on a surface whose background is transparent. `None` (default) declares nothing.
|
||||
|
||||
**Implement when external state matters**
|
||||
|
||||
@@ -282,7 +283,9 @@ The cheap things and the expensive things, in rough order:
|
||||
- *Avoid in `view()`*: cloning large `Vec<u8>` image buffers. `img_widget` takes an `Arc<Vec<u8>>`; build the `Arc` once at load time and clone *the Arc*, not the bytes.
|
||||
- *Avoid `is_animating() = true` when nothing is moving.* It pegs the loop at the full animation rate and burns battery on the mobile target.
|
||||
- *Lower `poll_interval()` is not free.* Crustace polls every 30 s because the clock only shows HH:MM. If your UI shows seconds, `Some(Duration::from_secs(1))` is fine; if it shows nothing time-sensitive, leave it `None`.
|
||||
- *Scroll viewports own a sub-canvas.* They're slightly more expensive to draw than a plain column. Use them when you need clipping or actual scrolling, not as a wrapper.
|
||||
- *Scroll viewports own a sub-canvas.* They're slightly more expensive to draw than a plain column. Use them when you need clipping or actual scrolling, not as a wrapper. A plain `viewport` without `fade_bottom` is cheaper: it clips with a scissor on the parent canvas and adds no offscreen pass, so it is the right wrapper for a panel whose visible height animates.
|
||||
- *Themed surfaces with shadows are pre-rendered on GLES.* A `container.surface(...)` whose slot carries outer or inset shadows (all with `Normal` blend) is drawn once per (size, corners, style) into a texture held by the context's cache and blitted afterwards, so a panel of a dozen glass cards costs a dozen textured quads per frame instead of six shader passes per card. Sliders and surfaces with `PlusLighter` / `Overlay` insets keep the direct path.
|
||||
- *Image textures are cached per GL context, not per canvas.* On the GLES backend every canvas and sub-canvas of a process shares one content-keyed texture cache (32 MiB, LRU), so an overlay or subsurface that is destroyed and recreated finds its icons and wallpaper resident; destroyed overlay canvases are pooled and reused too, shaders and glyph atlas included. Keep image buffers stable (`Arc` clones) so the fingerprint hits.
|
||||
- *GPU vs software*: the GLES path is selected automatically when EGL is available, with no API-level difference for the application. The two backends are not yet pixel-identical — gradients and the shadow / backdrop pipeline degrade under software; the authoritative list of gaps is the README's *Backend Differences* section.
|
||||
|
||||
When a redraw feels sluggish: add a one-line print at the top of `view()` and confirm it's not being called more often than expected. The single most common mistake is leaving `is_animating()` returning `true` after the animation finished.
|
||||
|
||||
Reference in New Issue
Block a user