vello_hybrid: A complete rewrite of the core logic (#1759) Also, I know the number of added/removed lines looks scary, but a significant part of this is just tests and also shuffling existing code around. š But obviously it's still a big PR! Initially, the plan was to try to split it up, but after discussing with Alex we've come to the conclusion that it makes more sense to deliver it as a whole "package". # Summary This PR implements a major rewrite of Vello Hybrid. The WebGL and WGPU backends as well as some core aspects of old Vello Hybrid (like how paints and filter are encoded remain mostly the same), but the core algorithm for interpreting and scheduling scene commands has been completely reworked. More information on what motivates this change can be found in an adjacent [Zulip thread](https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Removing.20coarse-rasterization.20based.20clipping/with/590142140) as well as in some render office hours transcripts, but to summarize it very briefly: Vello Hybrid is currently based on a so-called ācoarse rasterizationā approach, where render work is split up into wide tiles that can be rendered independently. One of the core motivations for doing so was to enable doing blending with bounded memory. However, as Vello Hybrid evolved, it turned out that the approach is not great, for two reasons. First, the CPU overhead for doing coarse rasterization is so big that it basically diminishes any of the performance characteristics we would expect from a GPU-based renderer. Secondly, coarse rasterization is fundamentally incompatible with filters, where the whole layer needs to be rendered on its own first. This goes against the core idea of corse rasterization, where the viewport is split up and it is assumed that each wide tile can be rendered independently. Because of the above, 1. Vello Hybrid now basically has two separate paths, the old coarse-rasterization based path and a new āfast strips pathā, which attempts to regain the performance benefits for simple scenes without layers. 2. In order to support filters, we had to resort to _a lot_ of hacks to make them work with coarse rasterization, especially to support them in combination with layer clipping. The code is incredibly complex and hard to reason about, making it hard to maintain it. There are also a few open issues with bugs. In addition, the current approach also makes it impossible to support filter layers that extend over the root viewport; currently, any filter effect that isn't in the main viewport area is cut off. Vello CPU was also plagued by similar problems, which weāve fixed by rewriting it to use a simpler coarse rasterization approach (see #1701), in my opinion with great success! This PR does the same, but for Vello Hybrid. Unlike Vello CPU, which does still need some form of coarse rasterization to work efficiently, for Vello Hybrid we do not need this at all. Therefore, it's completely removed. Instead, we just submit the "soup of strips" directly to the GPU and let it take care of the rest. From my testing, not only does it solve our existing problems, but it also greatly improves performance of layers, especially on low-tier devices, and makes the overall architecture of Vello Hybrid more consistent, better tested, much easier to reason about and more memory-efficient for scenes with many filters. # Showcases I will try to add some showcases tomorrow. # Changes I will not dive into the real "meat" of this PR too much here, because I've already extensively documented how it works in the code. But I will still give a list of the overall changes, also give a suggested order for reviewing this PR. ## vello_common Thanks to the preparatory PRs that have already been merged, the only changes are updating some references to files in comments. ## vello_sparse_shaders I've renamed the shader files to make their naming a bit more consistent. The blending logic has been extracted into a separate shader since it's a completely independent stage now. I've also added a new `copy` shader and repurposed the existing `clear_slots` shader. Other than that, I also did some renaming of variables in the existing shaders to make them consistent. Apart from that, everythin should be the same. ## vello_sparse_tests With this PR, we can finally get rid of the scene constraints, as we can completely transparently handle scenes where the root is a blend target, all while still retaining the best speed benefits for both cases! Apart from that, I've also added a bunch of new test cases to get more coverage for the new code. ## vello_hybrid Staring with the easier part, I've deleted the old `schedule` module, since it's replaced with a new algorithm. The fast rect path has been extracted into a new `rect` module and paint-handling into `paint`. The new modules `blend` and `copy` are for GPU-side preparation of those stages. I've also made changes to `filter`, but the core idea of how filters are handled is the same, except that it's completely integrated into the new layer-scheduling mechanism. As a consequence, we do not pollute the image atlas for filters anymore! `scene` has been rewritten to use the new recording mechanism, and the new `target` module provides. In `util`, I've added a new abstraction to allow viewing non-contiguous ranges of slices in a buffer as a single contiguous ones. Above are the easy changes, but from then on things get more tricky, and I'm unsure what the best way to review them is. š I would probably start by reading the doc comments of all modules in `schedule` to get the basic idea behind the new scheduling algorithm. Then, it probably makes sense to go bottom-up, by reviewing independent modules like `draw` (which should be familiar to already-existing code), `target`, followed by `allocate`, `execute` and `cursor`. Those basically form the building blocks of the new scheduling algorithm and can kind of be understood independently. After that, I'd recommend reading through `round`, which forms our representation of an independent set of render passes executed as a unit. And finally, the biggest part is in `schedule/mod.rs`, which ties all of the other elements together to implement what we need. # Follow-ups - With this PR merged, coarse rasterization in `vello_common` is essentially dead code. However, I will wait until this PR is merged before actually deleting the module, as there are some other places that still use `WideTile::WIDTH`, and I donāt want to make the diff larger than it already is! This PR was done with assistance of GPT 5.6 Sol, but it goes without saying that Iāve heavily guided and thoroughly reviewed this implementation, to make sure itās robust and lives up to our standards. Itās possible that a comment might have gotten lost here and there as part of moving code around, but I tried my best to find and fix those before opening this PR. Iāve also gone through a lot of iterations to arrive at this point, so itās possible there is some accidental or unnecessary complexity somewhere that I missed.
A GPU compute-centric 2D renderer
Vello is a 2D graphics rendering engine written in Rust, with a focus on GPU compute. It can draw large 2D scenes with interactive or near-interactive performance, using wgpu for GPU access.
Quickstart to run an example program:
cargo run -p with_winit
It is used as the rendering backend for Xilem, a Rust GUI toolkit.
[!WARNING] Vello can currently be considered in an alpha state. In particular, we're still working on the following:
Significant changes are documented in the changelog.
Vello is meant to fill the same place in the graphics stack as other vector graphics renderers like Skia, Cairo, and its predecessor project Piet. On a basic level, that means it provides tools to render shapes, images, gradients, text, etc, using a PostScript-inspired API, the same that powers SVG files and the browser <canvas> element.
Vello's selling point is that it gets better performance than other renderers by better leveraging the GPU. In traditional PostScript-style renderers, some steps of the render process like sorting and clipping either need to be handled in the CPU or done through the use of intermediary textures. Vello avoids this by using prefix-sum algorithms to parallelize work that usually needs to happen in sequence, so that work can be offloaded to the GPU with minimal use of temporary buffers.
This means that Vello needs a GPU with support for compute shaders to run.
Vello is meant to be integrated deep in UI render stacks. While drawing in a Vello scene is easy, actually rendering that scene to a surface requires setting up a wgpu context, which is a non-trivial task.
To use Vello as the renderer for your PDF reader / GUI toolkit / etc, your code will have to look roughly like this:
use vello::{ kurbo::{Affine, Circle}, peniko::{Color, Fill}, *, }; // Initialize wgpu and get handles let (width, height) = ...; let device: wgpu::Device = ...; let queue: wgpu::Queue = ...; let mut renderer = Renderer::new( &device, RendererOptions::default() ).expect("Failed to create renderer"); // Create scene and draw stuff in it let mut scene = vello::Scene::new(); scene.fill( vello::peniko::Fill::NonZero, vello::Affine::IDENTITY, vello::Color::from_rgb8(242, 140, 168), None, &vello::Circle::new((420.0, 200.0), 120.0), ); // Draw more stuff scene.push_layer(...); scene.fill(...); scene.stroke(...); scene.pop_layer(...); let texture = device.create_texture(&...); // Render to a wgpu Texture renderer .render_to_texture( &device, &queue, &scene, &texture, &vello::RenderParams { base_color: palette::css::BLACK, // Background color width, height, antialiasing_method: AaConfig::Msaa16, }, ) .expect("Failed to render to a texture"); // Do things with `texture`, such as blitting it to the Surface using // wgpu::util::TextureBlitter
See the examples directory for code that integrates with frameworks like winit.
We've observed 177 fps for the paris-30k test scene on an M1 Max, at a resolution of 1600 pixels square, which is excellent performance and represents something of a best case for the engine.
More formal benchmarks are on their way.
A separate Linebender integration for rendering SVG files is available through vello_svg.
A separate Linebender integration for playing Lottie animations is available through velato.
A separate Linebender integration for rendering raw scenes or Lottie and SVG files in Bevy through bevy_vello.
Our examples are provided in separate packages in the examples directory. This allows them to have independent dependencies and faster builds. Examples must be selected using the --package (or -p) Cargo flag.
Our winit example (examples/with_winit) demonstrates rendering to a winit window. By default, this renders the GhostScript Tiger as well as all SVG files you add in the examples/assets/downloads directory. A custom list of SVG file paths (and directories to render all SVG files from) can be provided as arguments instead. It also includes a collection of test scenes showing the capabilities of vello, which can be shown with --test-scenes.
cargo run -p with_winit
We aim to target all environments which can support WebGPU with the default limits. We defer to wgpu for this support. Other platforms are more tricky, and may require special building/running procedures.
Because Vello relies heavily on compute shaders, we rely on the emerging WebGPU standard to run on the web. Browser support for WebGPU is still evolving. Vello has been tested using production versions of Chrome, but WebGPU support in Firefox and Safari is still experimental. It may be necessary to use development browsers and explicitly enable WebGPU.
The following command builds and runs a web version of the winit demo. This uses cargo-run-wasm to build the example for web, and host a local server for it
# Make sure the Rust toolchain supports the wasm32 target rustup target add wasm32-unknown-unknown # The binary name must also be explicitly provided as it differs from the package name cargo run_wasm -p with_winit --bin with_winit_bin
There is also a web demo available here on supporting web browsers.
[!WARNING] The web is not currently a primary target for Vello, and WebGPU implementations are incomplete, so you might run into issues running this example.
The with_winit example supports running on Android, using cargo apk.
cargo apk run -p with_winit --lib
[!TIP] cargo apk doesn't support running in release mode without configuration. See their crates page docs (around
package.metadata.android.signing.<profile>).See also cargo-apk#16. To run in release mode, you must add the following to
examples/with_winit/Cargo.toml(changing$HOMEto your home directory):
[package.metadata.android.signing.release] path = "$HOME/.android/debug.keystore" keystore_password = "android"
[!NOTE] As
cargo apkdoes not allow passing command line arguments or environment variables to the app when ran, these can be embedded into the program at compile time (currently for Android only)with_winitcurrently supports the environment variables:
VELLO_STATIC_LOG, which is equivalent toRUST_LOGVELLO_STATIC_ARGS, which is equivalent to passing in command line arguments
For example (with unix shell environment variable syntax):
VELLO_STATIC_LOG="vello=trace" VELLO_STATIC_ARGS="--test-scenes" cargo apk run -p with_winit --lib
This version of Vello has been verified to compile with Rust 1.88 and later.
Future versions of Vello might increase the Rust version requirement. It will not be treated as a breaking change and as such can even happen with small patch releases.
As time has passed, some of Vello‘s dependencies could have released versions with a higher Rust requirement. If you encounter a compilation issue due to a dependency and don’t want to upgrade your Rust toolchain, then you could downgrade the dependency.
# Use the problematic dependency's name and version cargo update -p package_name --precise 0.1.1
Discussion of Vello development happens in the Linebender Zulip, specifically the #vello channel. All public content can be read without logging in.
Contributions are welcome by pull request. The Rust code of conduct applies.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache 2.0 license, shall be licensed as noted in the License section, without any additional terms or conditions.
Vello was previously known as piet-gpu. This prior incarnation used a custom cross-API hardware abstraction layer, called piet-gpu-hal, instead of wgpu.
An archive of this version can be found in the branches custom-hal-archive-with-shaders and custom-hal-archive. This succeeded the previous prototype, piet-metal, and included work adapted from piet-dx12.
The decision to lay down piet-gpu-hal in favor of WebGPU is discussed in detail in the blog post Requiem for piet-gpu-hal.
A vision document dated December 2020 explained the longer-term goals of the project, and how we might get there. Many of these items are out-of-date or completed, but it still may provide some useful background.
Vello takes inspiration from many other rendering projects, including:
Licensed under either of
at your option.
In addition, all files in the vello_shaders/shader and vello_shaders/src/cpu directories and subdirectories thereof are alternatively licensed under the Unlicense (vello_shaders/shader/UNLICENSE or http://unlicense.org/). For clarity, these files are also licensed under either of the above licenses. The intent is for this research to be used in as broad a context as possible.
The files in subdirectories of the examples/assets directory are licensed solely under their respective licenses, available in the LICENSE file in their directories.