Files
ltk/docs/backends.md
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

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