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