Render GPU-accelerated 2D vector graphics (bezier paths, fills, strokes,
gradients) and high-quality shaped text in Rust with the Linebender stack:
Vello (compute-based vector renderer on wgpu) + Parley (text shaping/layout)
+ kurbo (geometry) + peniko (brushes/color). Covers winit+wgpu surface setup,
building a Scene every frame, feeding Parley glyph runs into Vello, the
retained-vs-immediate question, vsync/frame pacing, and the version-pinning
minefield between vello/wgpu/parley. Activate on: "Vello", "Parley",
"Linebender", "GPU vector rendering in Rust", "render text with wgpu",
"kurbo bezier", "peniko brush", "draw_glyphs", "Scene::fill", "custom 2D GPU
canvas". NOT for: 3D rendering (use wgpu directly or bevy), Metal shader
authoring (use metal-text-pipeline / metal-shader-expert), web canvas
(use Canvas2D/WebGL), or native OS widgets (use gpui/egui/iced).
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Render GPU-accelerated 2D vector graphics (bezier paths, fills, strokes,
gradients) and high-quality shaped text in Rust with the Linebender stack:
Vello (compute-based vector renderer on wgpu) + Parley (text shaping/layout)
+ kurbo (geometry) + peniko (brushes/color). Covers winit+wgpu surface setup,
building a Scene every frame, feeding Parley glyph runs into Vello, the
retained-vs-immediate question, vsync/frame pacing, and the version-pinning
minefield between vello/wgpu/parley. Activate on: "Vello", "Parley",
"Linebender", "GPU vector rendering in Rust", "render text with wgpu",
"kurbo bezier", "peniko brush", "draw_glyphs", "Scene::fill", "custom 2D GPU
canvas". NOT for: 3D rendering (use wgpu directly or bevy), Metal shader
authoring (use metal-text-pipeline / metal-shader-expert), web canvas
(use Canvas2D/WebGL), or native OS widgets (use gpui/egui/iced).
allowed-tools
Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch
metadata
{"category":"Frontend & UI","tags":["vello","parley","wgpu","rust","gpu","vector-graphics","text-rendering","linebender","kurbo","peniko"],"pairs-with":[{"skill":"metal-text-pipeline","reason":"The bare-metal counterpart — what Vello hides, and when pure Metal is worth it."},{"skill":"metal-shader-expert","reason":"For when you drop below Vello into MSL compute/fragment shaders."},{"skill":"rust-app-distribution","reason":"Shipping the signed/notarized desktop binary that hosts the renderer."}],"provenance":{"kind":"first-party","owners":["port-daddy"]},"authorship":{"maintainers":["port-daddy"]}}
Vello + Parley GPU Vector Rendering (Rust)
Vello is a compute-shader 2D vector renderer: you hand it a Scene (a list
of filled/stroked paths, images, glyph runs) and it rasterizes the whole thing
on the GPU in a few compute passes. Parley is the text half: it shapes and
lays out Unicode runs (via HarfRust + Swash/Skrifa) and yields positioned
glyphs you stream straight into the same Vello Scene. Together they give you a
pixel-perfect, fully custom 2D canvas with no widget model in the way —
which is the entire reason you'd reach for them over gpui/egui.
This skill is the worked, version-pinned, gotcha-annotated path from "blank
window" to "anti-aliased beziers + crisp shaped text at 120fps." The worked
example is real: core/pd-timeline-proto/ in this repo renders a scrubable
timeline (tracks, event markers, causal bezier threads, a playhead) this way.
When To Use
You need a bespoke 2D visualization (timeline, node graph, waveform, map,
diagram) where every pixel is yours and a retained widget tree gets in the way.
You want resolution-independent vector output (smooth zoom, crisp on HiDPI)
without managing your own tessellation/rasterization.
You need high-quality text (real shaping, ligatures, variable fonts, RTL)
rather than a bitmap-font hack.
You're already on wgpu and want a 2D layer that composes with your 3D, or you
want one renderer that runs on Metal/Vulkan/DX12/GL from one codebase.
NOT For
3D scenes, meshes, lighting → use wgpu directly, or bevy.
Authoring the shaders themselves → metal-shader-expert (MSL) or wgpu WGSL.
The "should I even leave gpui?" decision and pure-Metal costs →
metal-text-pipeline.
Native OS controls, accessibility trees, text input widgets → gpui, egui,
iced. Vello draws; it does not give you a button or a focus ring.
Web targets → Canvas2D / WebGL / WebGPU bindings, not native Vello (Vello does
have a web path, but if web is your primary target start there).
Decision Points
1. Pick the stack layer you actually need
What are you drawing?
├─ Arbitrary 2D vector + text, custom layout
│ └─ Vello (Scene) + Parley (text) + kurbo (geom) + peniko (brush) ← this skill
├─ 2D but you want widgets/layout/events handed to you
│ └─ Xilem (Linebender's UI framework, built on Vello+Parley) or egui/iced
├─ Just static SVG → raster once
│ └─ resvg / usvg (CPU) is simpler; skip the GPU entirely
└─ 3D / GPGPU
└─ wgpu directly; Vello is the wrong tool
2. Windowing + GPU surface
Need a window?
├─ Cross-platform desktop → winit 0.30 (ApplicationHandler model)
│ └─ GPU via wgpu; Vello ships `vello::util::RenderContext` to manage
│ Instance/Adapter/Device/Surface for you — USE IT, don't hand-roll wgpu init
├─ Embedding in an existing wgpu app
│ └─ Construct `Renderer` against your existing Device; render to your texture
└─ Headless render-to-PNG (tests, thumbnails, CI)
└─ Vello can render to a wgpu texture you read back; no surface needed
3. Retained vs immediate (Vello's real model)
Vello is immediate from your side, retained on the GPU side. You rebuild a
Scene each frame with scene.reset() then re-fill/stroke/draw_glyphs.
This sounds wasteful; it isn't — scene encoding is CPU-cheap (microseconds for
thousands of primitives) and the GPU does the heavy raster.
Rebuilding the scene every frame is the NORMAL pattern.
├─ Static content, rare changes → still rebuild; it's fine. Don't over-engineer caching.
├─ Huge scenes (100k+ paths) → consider sub-scenes / append() of cached fragments
└─ NEVER try to "diff" or mutate a Scene in place — that's not the API.
4. Anti-aliasing config
RendererOptions.antialiasing_support + RenderParams.antialiasing_method must AGREE.
├─ Cheapest, great for UI/vector → AaSupport::area_only() + AaConfig::Area
├─ MSAA8/MSAA16 → heavier, only if Area shows artifacts on thin strokes
└─ Mismatch (support says Area, params ask Msaa16) → panic at render time.
5. Frame pacing
PresentMode?
├─ AutoVsync (default, recommended) → tear-free, caps to display (60/120 ProMotion)
├─ AutoNoVsync → uncapped; use ONLY to benchmark raw GPU headroom, not to ship
└─ On ProMotion you get 120fps free IF you request a redraw every frame.
winit only redraws on request — call window.request_redraw() to keep animating.
Failure Modes
1. Version skew between vello / wgpu / parley (the #1 time sink)
Symptoms: expected wgpu::Device, found wgpu::Device (two wgpu majors in
the tree); RenderSurface field/method not found; trait bound errors on
Brush.
Root cause: vello re-exports a specific wgpu major. If your Cargo.toml
pulls a different wgpu, you have two incompatible wgpu crates.
Fix: Let vello own the wgpu version. Either depend on vello only and use
vello::wgpu::…, or pin your direct wgpu to exactly the major vello uses
(vello 0.3 → wgpu 22; check cargo tree -i wgpu). Same discipline for parley:
vello 0.3 pairs with parley 0.2 / peniko 0.2 / kurbo 0.11.
Detect: cargo tree -d (the -d finds duplicate crates). Two wgpu
entries = you will fail to compile or get cryptic type errors.
2. Glyph normalized-coords type mismatch
Symptoms: expected &[F2Dot14], found &[i16] when calling
.normalized_coords(...) on draw_glyphs.
Root cause: Parley yields variation coords as &[i16]; Vello/Skrifa wants
&[NormalizedCoord] (a #[repr(transparent)] wrapper over the same i16 bits,
F2Dot14).
Fix: reinterpret the slice — they are layout-identical:
let coords: &[skrifa::raw::types::F2Dot14] = unsafe { std::slice::from_raw_parts(raw.as_ptr().cast(), raw.len()) };
(Both are 2-byte; this is the idiom the parley examples use.)
Fix: pin exact minors and read THAT version's examples/ (clone the tagged
repo or open docs.rs at the pinned version). Don't trust a blog post for a
different minor. peniko::Color::with_alpha_factor → multiply_alpha;
Layout::align(width, alignment) dropped its trailing bool.
4. Borrow-checker fights in the winit ApplicationHandler
Symptoms: "cannot borrow *self as immutable because it is also borrowed
as mutable" inside window_event.
Root cause: holding let state = self.state.as_mut() across calls to other
&self/&mut self helpers and disjoint fields.
Fix: pull the Arc<Window> + dims out up front into locals, drop the
state binding, then take short, scoped borrows. Access disjoint struct
fields directly (e.g. &self.context, self.renderers[i], &self.scene) so
the borrow checker can split the &mut self — don't funnel them through one
intermediate &mut state.
5. Black / empty window
Symptoms: window opens, renders nothing.
Checklist: (a) did you request_redraw() after resume/resize? winit won't
redraw on its own. (b) RenderParams.width/height must match the surface
config, not the logical size. (c) surface format from
surface.config.format must be what the Renderer was created with. (d) you
called surface_texture.present(). (e) scene.reset() then actually pushed
primitives.
6. HiDPI text/geometry at half/double size
Root cause: mixing logical and physical pixels. winit gives physical
surface sizes; your layout is usually in logical points.
Fix: pass window.scale_factor() into Parley's layout (ranged_builder
takes a scale) AND into your geometry transform, consistently. Decide one
coordinate space and convert at the boundary.
Worked Example
A minimal-but-complete loop (the shape core/pd-timeline-proto uses). Pinned to
vello 0.3 / wgpu 22 / parley 0.2 / peniko 0.2 / kurbo 0.11 / winit 0.30.
// Cargo.toml: vello="0.3" wgpu="22" winit="0.30" parley="0.2"// peniko="0.2" kurbo="0.11" pollster="0.4"use vello::util::{RenderContext, RenderSurface};
use vello::{AaConfig, AaSupport, Renderer, RendererOptions, RenderParams, Scene};
use vello::peniko::{Color, Fill};
use vello::wgpu; // <- use vello's re-exported wgpu, never a second oneuse kurbo::{Affine, Circle, Point, Stroke, Line};
// 1. Surface (winit Window -> wgpu Surface), let Vello's RenderContext drive wgpu init.letmut cx = RenderContext::new();
letsurface: RenderSurface =
pollster::block_on(cx.create_surface(window.clone(), w, h, wgpu::PresentMode::AutoVsync))?;
// 2. One Renderer per device. AA support must match the AA you ask for at render time.letmut renderer = Renderer::new(
&cx.devices[surface.dev_id].device,
RendererOptions {
surface_format: Some(surface.format),
use_cpu: false,
antialiasing_support: AaSupport::area_only(), // pairs with AaConfig::Area below
num_init_threads: None,
},
)?;
// 3. Per frame: rebuild the scene from scratch.letmut scene = Scene::new();
scene.reset();
scene.stroke(&Stroke::new(1.0), Affine::IDENTITY, Color::rgb8(0x2a,0x32,0x3c), None,
&Line::new(Point::new(0.0, 100.0), Point::new(800.0, 100.0)));
scene.fill(Fill::NonZero, Affine::IDENTITY, Color::rgb8(0x4f,0xd1,0x6b), None,
&Circle::new(Point::new(120.0, 100.0), 5.0));
// (text via Parley -> scene.draw_glyphs(...); see references/parley-glyph-runs.md)// 4. Render to the surface.lettex = surface.surface.get_current_texture()?;
letdev = &cx.devices[surface.dev_id];
renderer.render_to_surface(&dev.device, &dev.queue, &scene, &tex, &RenderParams {
base_color: Color::rgb8(0x0d,0x11,0x17),
width: w, height: h,
antialiasing_method: AaConfig::Area, // must agree with AaSupport above
})?;
tex.present();
window.request_redraw(); // keep animating; winit won't redraw on its own
Text (the Parley half), the part everyone gets stuck on: build a Layout,
break_all_lines, align, then for each PositionedLayoutItem::GlyphRun, call
scene.draw_glyphs(font).brush(..).transform(..).font_size(..).normalized_coords(coords).draw(Fill::NonZero, glyphs)
where glyphs is an iterator of vello::Glyph { id, x, y } you accumulate by
advancing x += g.advance. See references/parley-glyph-runs.md for the exact,
compile-checked function.
Anti-Patterns
Anti-Pattern 1: Pulling your own wgpu next to Vello's
# ❌ two wgpu majors in the tree -> type mismatches everywherevello = "0.3"wgpu = "0.20"# vello 0.3 wants wgpu 22
# ✅ either use vello::wgpu, or pin the SAME major vello usesvello = "0.3"wgpu = "22"# matches; verify with `cargo tree -d`
Anti-Pattern 2: Caching/diffing the Scene to "save work"
Rebuilding the scene each frame is the intended model and costs microseconds.
Building a retained scene-graph diff layer on top is wasted complexity and fights
the API. Only reach for Scene::append of cached sub-scenes when you have
genuinely huge (100k+ primitive) static regions, measured.
Anti-Pattern 3: Bitmap fonts / texture-atlas-by-hand for text
Vello + Parley already do GPU glyph rendering with real shaping. Hand-rolling a
bitmap atlas throws away ligatures, hinting, variable fonts, RTL/complex scripts,
and HiDPI crispness. If you think you need a manual atlas, you're below the right
layer — see metal-text-pipeline for when that's actually justified.
Anti-Pattern 4: Ignoring request_redraw and wondering why it's static
winit is event-driven. No redraw request → no frame. Animations, scrubbing, and
auto-play all require you to request the next redraw (from resumed, after input,
and at the end of a frame while animating).
Anti-Pattern 5: Trusting a tutorial for a different crate version
These crates are pre-1.0 and rename things between minors. A snippet for vello
0.2 will not compile against 0.3. Always read the examples/ in the tagged
release matching your pin.
References
references/version-matrix.md — known-good vello/wgpu/parley/peniko/kurbo/winit
version combinations and how to verify with cargo tree -d.
references/parley-glyph-runs.md — the exact, compile-checked
render_glyph_run function that feeds Parley output into a Vello Scene,
including the i16 -> F2Dot14 coords reinterpret.
Provenance
Worked example and gotchas verified against core/pd-timeline-proto/ in this
repo (a winit+wgpu+Vello+Parley "Voyage Timeline" window), built and run on
Apple M4 Max (Metal backend) at sub-millisecond per-frame GPU cost.
Skill Bundle Index
Every file in this skill, and when to open it. Auto-generated; run scripts/index_references.py --fix.
references/
references/parley-glyph-runs.md — Feeding Parley glyph runs into a Vello Scene — The single function everyone gets stuck writing.
references/version-matrix.md — Vello / wgpu / Parley version matrix — Pre-1.0 Linebender crates move in lockstep clusters.