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.
4.7 KiB
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. |
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).