impeller-hal 0.1.0

Rendering hardware abstraction layer for impeller-rs.
Documentation
# impeller-rs

A tessellation-based 2D vector graphics renderer for Rust, targeting everything
from desktop discrete GPUs down to embedded SoCs driving panels directly
through KMS with no compositor present.

> **Status: early development.** Solid fills, strokes, antialiasing, gradients
> (linear, radial, sweep), and the Porter-Duff blend modes render on both
> backends and are verified against real hardware and a software reference. The
> advanced blend modes — separable and non-separable both, all fifteen — render
> on Vulkan where the device offers the advanced-blend extension, and are
> reported as unavailable elsewhere rather than approximated. Clipping works on both
> backends for rectangles and arbitrary paths, nests, and antialiases with the
> shapes it confines. Images can be uploaded and drawn as a paint, with clamp,
> repeat, mirror and decal tiling, and with a source rectangle so a sprite
> sheet or a nine-patch is one upload, and a tint so one monochrome icon sheet
> serves every state, and gradients tile the same four ways past
> their own extent and take any number of color stops. Strokes dash, with the
> pattern measured along the path so it follows a curve rather than its chord.
> Paths take elliptical arcs, which is what a progress ring and a pie slice are
> made of. Save layers give a subtree its own target, so group
> opacity and layer-wide blend modes work and nest, and a layer can blur what is
> behind it -- frosted glass, not just a soft shadow. A glyph atlas packs
> caller-supplied single-channel coverage, evicts what a frame stops using,
> grows when it has nothing to evict, and draws a run of any length as one
> draw; bring your own rasterizer. Windowed presentation works on both backends
> and takes the same shape on each: the caller creates the surface — a
> `VkSurfaceKHR` or an `EGLSurface` — because owning that relationship would
> mean owning a windowing library. What no test here can see is what a window
> system actually puts on screen, so both paths are exercised against a surface
> with no window behind it and pin the property that decides it instead.

## Why

- **Tessellation-based, no compute shader requirement.** Runs correctly on
  every Vulkan 1.1+ device and every GLES 3.0 device, including embedded GPUs
  where compute is weak or driver support is immature.
- **Predictable frame times.** All pipelines are compiled ahead of time — no
  shader compilation jank, no driver-specific compute paths.
- **First-class direct scanout.** Most 2D renderers assume a windowing system
  exists. Here, rendering straight to a KMS plane with no compositor is a
  supported and tested configuration rather than an exercise left to the
  reader.
- **Pure-Rust source tree.** Every dependency compiles from Rust source. No C
  or C++ toolchain, no pkg-config, no system headers at build time, which keeps
  cross-compilation to aarch64 and riscv64 boards trivial.

## The shape of it

Two orthogonal axes. The **rendering HAL** answers how draw commands become
pixels in a GPU image. **Presentation** answers how a finished image reaches
the display, and how the frame loop is paced. DRM/KMS is a presentation target
alongside windowed surfaces, not a third rendering backend.

|            | WSI (windowed)      | DRM (direct scanout)                          |
|------------|---------------------|-----------------------------------------------|
| **Vulkan** | `VkSwapchainKHR`    | VkImage → dma-buf export → drm-rs FB → commit |
| **GLES**   | EGL window surface  | EGL on GBM → gbm_surface → drm-rs FB → commit |

All four are Tier 1 on Linux. The windowed column works today against a surface
the caller supplies. The scanout column is **partial**: the frame loop above
KMS is implemented and tested — the buffer ring, fence plumbing, and format and
modifier negotiation — and dma-buf export from Vulkan is real. A buffer this
renderer allocates, draws into and exports is accepted by a real display
controller as a framebuffer, the mode is set, and frames flip in turn — checked
against the virtual KMS driver, since a compositor holds master on any card
driving a display. The render fence rides each commit, so the
kernel latches the flip when rendering completes and the frame loop blocks on
nothing — except on a commit that also sets the mode, which vkms will not
complete with a fence attached and which happens once per output.

`cargo xtask drm` says whether a given machine could run that lane.

```rust
use impeller::{BackendPreference, Canvas, Color, Context, Extent2D, Paint, PixelFormat, Rect};

// The backend is chosen at run time, so one binary serves a board with a
// working Vulkan driver and one where only GLES is usable.
let mut ctx = Context::new(BackendPreference::Auto)?;

let size = Extent2D::new(256, 256);
let mut surface = ctx.create_surface(size, PixelFormat::Rgba8Unorm)?;

let mut canvas = Canvas::new(size);
canvas.clear(Color::WHITE);
canvas.draw_rect(
    Rect::new(32.0, 32.0, 224.0, 224.0),
    &Paint::fill(Color::rgba8(0, 120, 220, 255)),
)?;

// Recording is separate from submitting, so a whole frame is described
// before any of it reaches the GPU and every shape shares one pass.
ctx.draw(&mut surface, &canvas.finish())?;
let pixels = ctx.read(&mut surface)?;
```

## Running it

```sh
cargo run -p impeller-rs --example frame -- frame.ppm
```

Draws one frame through the public API — a gradient, a group composited through
a layer, an image sampled through a path clip, a blend mode, and a run of
glyphs — and writes a binary PPM, which needs no encoder. Image encoding is out
of scope, so converting to something friendlier is `magick frame.ppm frame.png`.

## How much of a renderer this is

[`docs/parity.md`](https://github.com/jwinarske/impeller-rs/blob/main/docs/parity.md) is the operation-by-operation comparison
against the `dart:ui` `Canvas` and `Paint` surface — the contract a
Flutter-class renderer owes, and a more useful yardstick than any one
implementation's internals. It distinguishes what exists from what a caller
could assemble, and every row claiming something works names the scene or test
that renders it.

[`docs/non-parity.md`](https://github.com/jwinarske/impeller-rs/blob/main/docs/non-parity.md) is the other half of that question.
Technical parity with upstream Impeller is what this project decides against,
so the places it knowingly does not have parity are worth being able to find in
one list rather than inferring from a diff. Each entry says what differs, why,
and what the difference costs. The deepest is that this pipeline works in linear
light and upstream's does not, which is why a comparison against upstream can
only be a comparison of shape wherever two colors are mixed.

## Building

Requires a Rust toolchain; no other build dependencies. MSRV is 1.85, which is
where edition 2024 stabilized and is a floor the dependency graph sets rather
than the code: naga translates the shaders at build time and reaches indexmap.
CI builds the workspace on exactly that toolchain, so this figure is checked.

`rust-toolchain.toml` pins the compiler this is *built* with to 1.94.1, which
is a separate question from the floor and answered from a different place: it
is the version Yocto's wrynose release ships, so a build here uses the compiler
an embedded target will. The floor stays lower deliberately — at 1.85 it also
admits whinlatter's 1.90, which is checked. Walnascar's 1.84.1 misses by one
release on a dependency's edition, and scarthgap's 1.75 would cost a `naga`
downgrade that changes the emitted SPIR-V; `docs/architecture.md` has the
measurements.

```sh
cargo build                                   # default: vulkan + WSI
cargo build --features gles,present-egl       # GLES into an EGL window
cargo build --features gles,drm               # the classic embedded GBM path
cargo build --features vulkan,gles,drm        # one binary that picks at runtime
cargo test
```

The two axes are named separately because they are separate. `drm` names no
backend and composes with either, which is the orthogonality the whole design
rests on. `present-wsi` and `present-egl` each imply a backend, not because the
axes are coupled but because a Vulkan swapchain is Vulkan's own object and an
EGL surface is GLES's.

A test that finds no device prints why and returns, and `cargo test` captures
the output of a passing test — so a run with nothing to run on looks exactly
like a run that covered everything. `cargo xtask verify` runs the same suite
and prints what did *not* run, which is the number worth reading.

`drm` is presentation-only and composes with either rendering backend. Feature
flags live on the `impeller` facade crate, since a virtual workspace root
cannot declare them.

Development tasks run through `cargo xtask`. `report` says what this machine's
devices can do, `drm` whether it can drive a display directly, `verify` runs
the suite and reports what did *not* run, and `gallery` renders every corpus
scene onto one sheet to look at. `cargo xtask help` lists them.

## Layout

```
crates/
  impeller              public facade; carries the feature flags
  impeller-core         drawing API: Canvas, Paint, Color, recording, execution
  impeller-entity       entity and contents layer -- coverage only, not yet routed
  impeller-geometry     path types, flattening, tessellation, stroking, dashing
  impeller-renderer     render pass encoding, generic over the HAL
  impeller-text         glyph atlas packing and placement; bring your own rasterizer
  impeller-hal          rendering HAL trait
  impeller-hal-vulkan   Vulkan backend (first-class)
  impeller-hal-gles     GLES 3.0 backend
  impeller-present      presentation trait, format negotiation
  impeller-present-vk   Vulkan WSI swapchain target
  impeller-present-egl  EGL window-surface target
  impeller-present-drm  DRM/KMS scanout target
  impeller-shaders      WGSL sources, build-time translation via naga
  impeller-testkit      shared test harness
  impeller-capi         Impeller C API (libimpeller), ABI-compatible
xtask/                  device runs, golden management, CI reproduction
docs/                   architecture
```

## Impeller C API

`impeller-capi` builds `libimpeller`, intended as an ABI-compatible
implementation of upstream Impeller's C API so that a consumer linking that API
could link this instead without recompiling. **It is barely started**: version
negotiation is the only entry point, and the rest waits on a vendored copy of
the upstream header — guessing an enum value or a struct layout would produce a
library that links and then corrupts memory.

It is **not** a drop-in for Impeller inside the Flutter Engine build: the
engine compiles Impeller's C++ sources directly rather than consuming them
across this boundary, and no Rust library can present a compatible C++ ABI. The
C API serves embedders. See
[`docs/architecture.md`](https://github.com/jwinarske/impeller-rs/blob/main/docs/architecture.md#impeller-c-api-compatibility) for
where parity is partial and how it is verified.

## Scope

Bring your own text shaping and layout (`cosmic-text`, `parley`), font parsing
(`ttf-parser`, `swash`), image decoding (`image`), and SVG parsing (`usvg`).
Scene graph, retained mode, animation, and 3D are out of scope, as is GLES 2.0.

This is a renderer, not a compositor: it draws to planes it is given.
Multi-client composition belongs elsewhere. KMS internals belong outside it,
and which outside differs by concern: connector and plane probing is drm-rs's,
while EDID parsing, mode selection policy, session and seat handoff, and
hotplug detection are the application's or its session manager's — drm-rs
reports the modes but does not choose one, and hotplug needs udev, which
nothing here uses.

## Documentation

[`docs/architecture.md`](https://github.com/jwinarske/impeller-rs/blob/main/docs/architecture.md) covers the design and the rules
that govern the codebase: the HAL and presentation split, the Vulkan-first
policy, explicit synchronization, the ownership boundary with drm-rs, format
and modifier negotiation, the shader pipeline, dependency purity, and the
testing model. Read it before proposing structural changes — a fair number of
alternatives were considered and rejected for recorded reasons.

[`docs/on-a-board.md`](https://github.com/jwinarske/impeller-rs/blob/main/docs/on-a-board.md) is how to cross-build the suite and
run it on a real device, and what doing so has found. Most of what this suite
checks is agreement between two devices, and on a workstation both of them are
software — so the board is not a nice-to-have lane, it is where a class of
defect is visible at all.

## Releases

Nothing is released. `impeller-rs 0.0.0` exists on crates.io and contains **no
API**: it reserves the name, which the plain `impeller` had already lost to an
unrelated crate, and it says so in its own description. Do not depend on it;
the first version with anything in it will be `0.1.0` or later.

Fourteen of this workspace's seventeen crates are `publish = true`; `xtask`,
`impeller-testkit` and `impeller-capi` refuse, each saying why in its own manifest
and each one line from changing its mind. Nothing has been published under those
settings yet, so the gate that remains is a person running `cargo publish`. The manifests are otherwise ready: versions on internal
dependencies, metadata filled in, this file wired in as each crate's `readme` by
symlink so there is no second copy to go stale, and `all-features` set for docs.rs
on `impeller-rs`, whose default features are Vulkan and its swapchain and would
otherwise document neither the GLES backend nor either presentation path. The links
above are absolute for the same reason -- a relative one resolves here and 404s on
a crate page.

Two things about a first publish are worth knowing before starting one. It cannot
be rehearsed: `cargo package` resolves against the crates.io index even with
`--no-verify`, so today only `impeller-geometry` and `impeller-hal` can be packaged
and everything else fails on a dependency that is not there yet. And it is
therefore strictly ordered, each crate unblocking the next --  the two leaves, then
`renderer` and `text`, then `core`, then `capi`, `entity` and `shaders`, then the
two backends, then `present` and its three paths, then `impeller-rs`, with
`testkit` last if it goes at all. [`CHANGELOG.md`](https://github.com/jwinarske/impeller-rs/blob/main/CHANGELOG.md)
tracks what would go into the first release that has one.

## Contributing

Run these in order before committing, and do not commit on a failure:

```sh
cargo clippy --workspace --all-targets --fix --allow-dirty   # lint, applying fixes
cargo fmt --all                                              # format
cargo build --workspace --all-targets                        # smoke test
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps   # resolve doc links
cargo test --workspace
```

`cargo xtask gate` is all of that as one exit code. Read the test count rather
than the exit code when running the pieces by hand: a suite that compiled
nothing and a suite that passed everything both exit zero.

It also prints what the suite said about its own coverage -- how many catalog
plates were drawn and how many compared -- because those are different numbers
and only one of them is in the total. A plate needing a capability one backend
lacks is reported and skipped, correctly, and the suite still passes; "834
passed" reads the same whether it compared every plate or four fifths of them.

Read `gate`'s own output whole, though, and not through a filter. Test binaries
run in parallel and one can write over another's summary line; the count is then
short by however many tests that binary held, and by its failures too. That is
detected and said out loud -- "N test binaries said nothing this could read" --
and the run fails. But it is said in a line that a grep for `passed` or `FAILED`
does not match, and a pipe hides the exit code that would have caught it. A
count four short with no failures and no skips is what that looks like from
behind a narrow filter.

**A green gate is not a green CI, and the difference is not cosmetic.** CI runs
the same suite against lavapipe with software GL forced, and with the Vulkan
validation layer installed. Two things follow. A device the machine in front of
you does not have will disagree — one lavapipe version writes outside a scissor
where three other drivers do not, and a threshold fitted to one GPU can clear it
by one per cent there and fail everywhere else. And the validation layer is the
only thing that reports a Vulkan object outliving its device; without it those
tests still pass, and on a machine that has no layer they say so in skips nobody
reads. A hundred and forty-nine of them, measured on 2026-09-02 rather than
estimated, and measurable again in one command:

```sh
mkdir -p /tmp/no-layers && VK_LAYER_PATH=/tmp/no-layers cargo xtask verify
```

Which hides the layer's manifest from the loader and leaves everything else
alone: the same 954 tests pass either way, with six skips when the layer is
there and a hundred and fifty-five when it is not. This said a hundred and
twenty once and the suite outgrew it, which is what the command is for.

The gate says what CI last said, in a line beside the skip census and the
timing drift, and for the same reason both of those are there: it is something
this machine cannot check, so it is reported rather than enforced. It went
unreported once and CI stayed red for twenty-six commits on one assertion while
every local gate passed. No `gh`, no network or no run is itself a line -- the
outcome this must not have is silence.

To run what CI runs:

```sh
cargo xtask verify --software
```

Which finds the CPU drivers wherever the distribution put them and prints the
variables it set, so the run says which drivers answered it. `gate` takes the
flag too.

The feature axes are meant to compose independently, so check that they still
do — a backend and a presentation path are orthogonal, and a combination that
only builds because another feature happened to be on is a coupling:

```sh
for f in vulkan gles vulkan,gles,drm present-wsi gles,present-egl vulkan,gles,drm,present-wsi,present-egl; do
  cargo check -p impeller-rs --no-default-features --features "$f" || break
done
```

Two conventions about writing rather than building. American English
throughout — code, comments, documentation, commit messages. And a commit
message should explain why a change is shaped the way it is rather than
restate what moved, in prose paragraphs rather than bullet lists; the diff
already says what changed, and the reasoning is the part that is expensive to
recover later.

## License

BSD 3-Clause. See [`LICENSE`](https://github.com/jwinarske/impeller-rs/blob/main/LICENSE).

This matches the Flutter Engine, home of the C++ Impeller whose architecture
this project takes as its reference. Any code ported from there retains its
original copyright notice alongside this project's.