impeller-geometry 0.1.0

Path types, flattening, tessellation, stroking and dashing for impeller-rs.
Documentation
  • Coverage
  • 69.01%
    98 out of 142 items documented0 out of 101 items with examples
  • Size
  • Source code size: 258.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.8 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 4s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • Repository
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • jwinarske

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.

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

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

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 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 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 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 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:

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:

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:

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:

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.

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.