Implement an initial version of Vello API#1299
Implement an initial version of Vello API#1299DJMcNab wants to merge 25 commits intolinebender:mainfrom
Conversation
b200d5e to
a48f6c5
Compare
| // TODO: Maybe a better type is `atomicow::CowArc<'static, str>` | ||
| pub label: Option<&'static str>, |
There was a problem hiding this comment.
https://lib.rs/crates/smol_str is another option here (Winit depends on it)
There was a problem hiding this comment.
I'm wondering how much effort it would be to coordinate a UI-ecosystem-wide change to one of the other mini-string crates. IIRC some of them have more features.
Maybe at next RustWeek.
PoignardAzur
left a comment
There was a problem hiding this comment.
Overall, not sure how to think about this PR. It has a lot of TODO comments and hedges in places where we'll need to make judgment calls sooner or later.
| // This needing to be turbofished shows a real footgun with the proped "push_layer" API here. | ||
| None::<BezPath>, |
There was a problem hiding this comment.
I don't think we'll solve this in the short-term, but this is definitely worth creating an issue about.
| // TODO: Change module and give a better name. | ||
| pub type OurBrush = peniko::Brush<peniko::ImageBrush<TextureHandle>>; |
There was a problem hiding this comment.
I'd suggest "VelloBrush" for now.
| fn push_layer( | ||
| &mut self, | ||
| clip_transform: Affine, | ||
| clip_path: Option<impl Shape>, | ||
| blend_mode: Option<BlendMode>, | ||
| opacity: Option<f32>, | ||
| // mask: Option<Mask>, | ||
| // filter: Option<Filter> | ||
| ); |
There was a problem hiding this comment.
Is there any reason why the API doesn't let you add transform layers?
There was a problem hiding this comment.
It isn't an existing feature in any of the Vellos {Classic, Hybrid, CPU}.
It also doesn't provide new functionality over the user manually keeping track of a transform externally, but it makes reasoning about a number of things more difficult (primarily drawing hinted glyphs, but also in the future how filter effects and masks will be applied).
Implementation wise, it's also a bit confused because it doesn't behave in the same way as any other layer type; i.e. it would be a layer type which doesn't implement a new isolated context, unless you set any of the other fields.
| self.commands | ||
| .extend(other_commands.iter().map(|command| match command { | ||
| RenderCommand::DrawPath(transform, path) => { | ||
| RenderCommand::DrawPath(correct_transform(*transform), correct_path(*path)) | ||
| } | ||
| RenderCommand::PushLayer(command) => RenderCommand::PushLayer(PushLayerCommand { | ||
| clip_transform: correct_transform(command.clip_transform), | ||
| clip_path: command.clip_path.map(correct_path), | ||
| blend_mode: command.blend_mode, | ||
| opacity: command.opacity, | ||
| }), | ||
| RenderCommand::PopLayer => RenderCommand::PopLayer, | ||
| RenderCommand::SetPaint(affine, brush) => { | ||
| // Don't update the paint_transform, as it's already path local. | ||
| RenderCommand::SetPaint(*affine, brush.clone()) | ||
| } | ||
| RenderCommand::BlurredRoundedRectPaint(brush) => { | ||
| // Don't update the paint_transform, as it's (currently) already path local. | ||
| RenderCommand::BlurredRoundedRectPaint(brush.clone()) | ||
| } | ||
| })); |
There was a problem hiding this comment.
For example, seems like this block of code could be replaced with a transform layer.
There was a problem hiding this comment.
I don't think that's true; this code is equivalent to the implementation of Scene::append in Vello Classic. That code would still need to exist in a version of this proposal with transform layers; the path references still need to be updated.
The only part of this code which would change is the two calls to correct_transform; instead those would be handled by the transform layer.
| // TODO: Format? Premultiplication? Hdr? | ||
| // TODO: Explicit atlasing? |
There was a problem hiding this comment.
If you're not sure what fields you'll add, this struct should be #[non_exhaustive].
There was a problem hiding this comment.
Can you clarify how you expect this to work, if users need to create this struct?
This type mirrors the descriptors from wgpu, which aren't non_exhaustive.
Hopefully we get default_field_values soon enough, ideally with a nice non_exhaustive story.
sparse_strips/vello_api/src/scene.rs
Outdated
| #[derive(Debug)] | ||
| pub struct Scene { | ||
| pub paths: PathSet, | ||
| pub commands: Vec<RenderCommand>, | ||
| // TODO: Make this an `Option`? That is, allow explicitly renderer-agnostic scenes | ||
| // which also don't allow bringing in textures? | ||
| pub renderer: Arc<dyn Renderer>, | ||
| pub hinted: bool, | ||
| /// Handles for each texture stored in this scene. | ||
| /// | ||
| /// We store these to ensure that the textures won't be freed until this scene | ||
| /// is fully rendered (and dropped). | ||
| // TODO: HashSet? We'd need to bring in hashbrown, but that's probably fine | ||
| pub textures: Vec<TextureHandle>, | ||
| } |
There was a problem hiding this comment.
The Scene type needs way more documentation. As it is, I don't know how it's supposed to be used, how it fields relate to each other, what assumptions it makes that mustn't be broken, etc.
It's especially not clear to me how Scene and PathSet relate. Does each scene store its own set of paths? Doesn't that kind of defeat the point of path caching?
There was a problem hiding this comment.
It's especially not clear to me how
SceneandPathSetrelate. Does each scene store its own set of paths? Doesn't that kind of defeat the point of path caching?
Sure, this is definitely under-documented. I'm planning to spend the time to add some more docs as my near-term priority here.
Essentially, each Scene accumulates the paths which it's drawing. These can either be copied into any painters which the scene is used in, to be rasterised in a just-in-time manner, or moved into the renderer (i.e. cached).
This accumulation pattern is designed to avoid an allocation for each path, whilst still allowing reference-count based garbage collection for cached paths (they are reference counted as a group of "related" paths).
But whilst PathSet currently exists for that design, the caching is not yet implemented (as it doesn't actually exist in the underlying renderers, for one). But I feel it still makes sense as an abstraction boundary to have, albeit with better docs...
|
Great job, @DJMcNab. This looks like a strong foundational implementation, and designing a unified API is genuinely hard work. I haven’t dived deeply into the details yet, but I did a brief skim. My initial impression, which might be off, is that this PR introduces a lot of new abstractions and concepts at once. Because of that density, I worry that once this lands it may effectively become the finished API, with some parts receiving less thorough review than they deserve. At the same time, many of these new concepts could naturally spark discussion across different consumers. Not sure if this is feasible, but would it be possible to extract some of the core logic into multiple, smaller PRs and introduce things iteratively? That could make reviews simpler, reduce cognitive load, and ultimately improve review quality. The idea would be to break this into multiple small, logically scoped PRs, starting with traits and then layering in initial methods and implementations. I know this would be more time consuming, but in some cases it may be worth it. Keen to hear your thoughts. |
I think this is viable. The most obvious split is:
I don't think it's easy to have a much smaller scope. This hadn't seemed viable before designing the API, but it does now seem more so. |
This is a core part of #1299, split out to be re-viewable separately. One observation which I hadn't properly engaged with until today is that the form proposed in this PR is *independently* useful. That is, useful apps could be made renderer-agnostic with just this API, with none of the "renderer abstraction" pain of #1299. I do think that the renderer API in #1299 does have valuable aspects (having textures be reference counted is still something I really want us to have), but it isn't needed to get something off-the-ground. The core of this PR is two things: 1) The `PaintScene` trait. This is your standard 2d imaging model trait (comparable e.g. with web canvas, [`vello::Scene`](https://docs.rs/vello/latest/vello/struct.Scene.html), [`anyrender::PaintScene`](https://docs.rs/anyrender/latest/anyrender/trait.PaintScene.html)). 2) A renderer-erased implementation of that trait, called `vello_api::Scene`. I call it renderer-erased, because it is (or... will be) invalid to combine renderers within a Scene graph. That's because of image resources and the like. This PR does not have glyph support, and indeed I still believe that Vello API should continue to not have that.
This is an initial implementation of Vello API, our abstraction between Vello CPU and Vello Hybrid (and also maybe Vello Classic).
A few notes:
no_std+alloccompatible.Importantly, this PR is intentionally not making internal changes to the extant crates, and instead is only adding new API surface for the abstraction. The plan would be to make these changes only once this has landed, to limit merge conflicts as much as possible.
I think it's valuable to get something landed here, whether or not it ends up being the final state. The main advantage there is that it allows porting tests in a piecemeal fashion, as well as making changes to the APIs of Vellos CPU and Hybrid (without creating a massive merge conflict headache). This PR is currently scoped small enough to be easily revertible if we want to go a different direction.
A previous version of this PR used the renderer abstraction for the tests. That code is now gone as it wasn't landable, but can be seen in b200d5e and earlier.