Files
ltk/docs/backends.md
T
admin 53e7ccc46e
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
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.
2026-09-18 09:55:09 +02:00

34 lines
4.8 KiB
Markdown

# Backend capability matrix
ltk renders through one of two interchangeable backends behind the same `Canvas` API: **software** (CPU rasterisation with tiny-skia + fontdue into a `wl_shm` buffer) and **GLES** (GPU rasterisation via EGL + OpenGL ES 2/3). `ltk::run()` selects GLES when an EGL context can be created and falls back to software; an embedder driving `core::UiSurface` can force either. `Canvas::is_software()` lets a caller branch on the active backend at draw time.
The two backends are kept at visual parity for the common primitives — solid fills, strokes, lines, text, images, vector paths. This page is the canonical statement of where they differ, so the information does not have to be reassembled from per-method rustdoc. When a difference listed here is closed, update this table in the same patch.
## Matrix
| Capability | Software | GLES | Notes |
| --- | --- | --- | --- |
| Solid fills, strokes, lines (`fill_rect`, `stroke_rect`, `draw_line`) | Yes | Yes | Visual parity. A diagonal `draw_line` is one rotated quad on GLES (antialiased, round caps); software strokes a path. |
| Text (`draw_text`, `draw_text_with_font`) | Yes | Yes | Both snap pen positions to integer pixels; glyph caches differ (per-canvas hashmap with eviction vs. shared atlas texture) but output is at parity. |
| Images (`draw_image_data`) | Yes | Yes | Both snap destinations to integer pixels. GLES caches uploaded textures content-keyed, bounded to 32 MiB with LRU eviction; software draws directly from the buffer. |
| External GL texture (`draw_external_texture`, `External::texture`) | No-op | Yes | Software has no GL state to sample from. Use `External::cpu` for a backend-independent immediate-mode draw hook. |
| Vector paths (`fill_path`, `stroke_path`) | Yes | Yes | Both rasterise through the same tiny-skia code, so shapes are at parity. GLES uploads the rasterised path as a texture per draw — costly for large animated vectors. |
| Rect clipping (`set_clip_rects`) | Exact multi-rect mask | Bounding-box scissor | On GLES several dirty rects clip to their union; software masks each rect exactly. Branch on `is_software()` when exactness matters. |
| Path clipping (`set_clip_path`) | Yes (coverage mask) | Yes (offscreen layer + anti-aliased coverage composite) | Visual parity; different mechanisms. |
| Gradients (`fill_paint_rect` with `Linear` / `Radial`) | Flat fill from the first stop | Dedicated shaders | Software gradient rendering is a known gap; tiny-skia could render them natively but is not wired up yet. |
| Gradient interpolation spaces | `srgb`, `linear-rgb` | `srgb`, `linear-rgb` | `oklab` is declared in the schema but currently falls back to linear-light **on both backends** (shared LUT builder). |
| Outer drop shadows (`fill_shadow_outer`) | No-op | Analytic soft-shadow shader | Software shadow rendering is a known gap. |
| Inset shadows (`fill_shadow_inset`) | No-op | Yes, with `normal` / `plus-lighter` / `multiply` / `screen` / `overlay` blends | `overlay` routes through an FBO snapshot on GLES. |
| Backdrop blur (Glass surfaces with a `backdrop` block) | Not rendered | Yes (aux-FBO Gaussian pipeline) | Themes ship `-flat` surface variants without `backdrop`; nothing selects them per backend automatically — pass one explicitly when targeting software. |
| Bottom-edge fade (`blit_fade_bottom`, `viewport.fade_bottom`) | Hard edge | Feathered | Software ignores `fade_bottom_px`. |
| Cross-backend blit (`blit` between a software and a GLES canvas) | — | — | Unimplemented (panics). Never occurs in practice: sub-canvases always share their parent's backend. |
| Pixel readback (`read_rgba_pixels`) | Yes (un-premultiplies) | Yes | Both return tightly packed straight-alpha RGBA8. |
| Presentation | `write_to_wayland_buf` into `wl_shm` | `eglSwapBuffers` (with damage when available) | `write_to_wayland_buf` is a no-op on GLES; `present` is the GLES path. |
| Animation frame-rate cap | ~30 Hz default (overridable via `App::cap_software_animation`) | Uncapped (compositor frame callbacks) | The cap protects the CPU path from sustained full-rate rasterisation. |
## Practical guidance
For a theme or app that must look identical on both backends, stay within the parity set: solid fills, strokes, text, images, paths, path clips. The visible divergences are gradients, shadows, backdrop blur and the bottom fade — all degrade gracefully on software (flat fill, no shadow, no blur, hard edge) rather than failing, so a software rendering of a GLES-designed theme is flatter but functional.
The pixel-level test suite runs on the software backend; the GLES paths that need a live GL context (layer composites, shaders) are exercised by the examples. There are currently no golden tests comparing the two backends' output — parity for the common primitives is maintained by review and by the shared rasterisation code where it exists (paths).