Skip to content

Commit 1aadf4c

Browse files
committed
Moved each design into it's on file for clarity;
Expanded more on the custom widget design & API, finished a rough prototype; Made it more clear what the final API exposed to the user would be in each design case
1 parent f350571 commit 1aadf4c

File tree

5 files changed

+432
-152
lines changed

5 files changed

+432
-152
lines changed

text/23-custom-pipelines.md

Lines changed: 62 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -55,124 +55,55 @@ hops? Could we do this with a minimal unsafe abstraction layer? Should we even d
5555
![](silvia.jpeg)
5656

5757
As you can see, this is a somewhat complex topic with a lot of tradeoffs between implementation strategies. Of
58-
course, I would love if there was a better idea floating around out there that I haven't thought of! That being
59-
said, **here are some of the concepts that you will need to understand before you can understand what this
60-
RFC aims to address.**
58+
course, I would love if there was a better idea floating around out there that I haven't thought of! That being said,
59+
here are a few different designs & their concepts that would need to be introduced to Iced. I've broken them into
60+
their own markdown files for an easier time reading!
6161

62-
🖼 **Custom Pipelines**
62+
1) [Custom Primitive Pointer Design](designs/23-01-pointer.md)
63+
2) [Custom Shader Widget Design](designs/23-02-widget.md)
64+
3) [Multi-Backend Design](designs/23-03-multi-backend.md)
6365

64-
This is essentially just a regular ol' wgpu pipeline implementation, except one that isn't already integrated into
65-
Iced! This can be as simple or complex as you want it to be. For example, in a prototype that I made to render a
66-
simple triangle, this was as simple as this struct:
67-
68-
```rust
69-
pub struct CustomPipeline {
70-
pipeline: wgpu::RenderPipeline,
71-
vertices: wgpu::Buffer,
72-
}
73-
```
74-
75-
In Iced, `Primitive`s are mapped internally to the appropriate pipeline, though have no direct relationship to each
76-
other (for instance, a `Pipeline` doesn't have a primitive type `P`). Each is chosen manually for what is
77-
appropriate. There is currently also no abstraction for what a `Pipeline` actually is; by their nature they are all
78-
somewhat unique from each other, with minor underlying similarities (for example, every render pipeline must at some
79-
point allocate some data to a `wgpu::Buffer` & submit a `draw` command).
66+
All of these designs must be flagged under `wgpu`, unless we wanted to do some kind of fallback for tiny-skia which
67+
I don't think is viable. What would we fall back to for the software renderer if a user tries to render a 3D object,
68+
which tiny-skia does not support? Blue screen? :P
8069

70+
Overall, I'm the most happy with design #3 and think that it offers the most flexibility for advanced users to
71+
truly render anything they want.
8172

82-
💠 **Custom Primitives**
8373

84-
What, exactly, pray tell, are we rendering? Ultimately this is some chunk of data that gets used by a custom
85-
pipeline. This could take the form of data that's passed directly to the existing `Primitive` enum (like
86-
current `Primitive`s are), or something as simple as a single pointer.
87-
88-
One implementation might mean that a custom primitive could be defined within the existing `Primitive` enum as just a
89-
pointer to some pipeline state that implements certain methods required for rendering.
90-
91-
```rust
92-
pub enum Primitive {
93-
//...,
94-
Custom {
95-
id: u64, // a pipeline reference ID
96-
pipeline_init: fn(device: &wgpu::Device, format: wgpu::TextureFormat) -> Box<dyn Renderable + 'static>,
97-
// where "Renderable" defines a set of methods necessary for drawing
98-
}
99-
}
100-
```
101-
102-
Another implementation might define a custom primitive as a generic type that is unique to a `Renderer` or `Backend`.
74+
## 🎯 Implementation strategy
10375

104-
```rust
105-
pub trait Backend<Primitive> {
106-
//...
107-
}
108-
```
76+
### 🙌 #1: Custom Primitive Pointer Design
10977

110-
🧅 **Layers**
78+
Behind the scenes, this would require very little changes to Iced!
11179

112-
In Iced, layering currently happens at the presentation level. Primitives are submitted to the `Renderer` in a
113-
queue-like fashion, and are then grouped before being drawn back to front. This allows primitives that are meant to be
114-
rendered together to be transformed together, somewhat like a scene. For example, when clipping primitives let's
115-
say in a `Canvas`, this will create a new layer. Note that every layer added to the layer stack will incur
116-
additional performance costs with pipeline switching & extra draw commands submitted to the GPU!
80+
A `Primitive::Custom` variant would need to be added to the existing `iced_graphics::Primitive` enum, in order to
81+
have a way to pass a pipeline pointer to the `Renderer` and indicate its proper order in the primitive stack.
11782

118-
When considering a layering approach for custom primitives, we must think about how they are processed. Should a
119-
custom primitive be included in the existing `iced_wgpu::Layer` with some sort of ID matching?
83+
We would also need to add a new field to an `iced_wgpu::Layer`, something along the lines of:
12084

12185
```rust
12286
pub struct Layer<'a> {
123-
//...
124-
// some kind of reference to what can be rendered in this layer that
125-
// we can match to a pipeline
126-
custom: Vec<PipelineId>,
87+
//...
88+
custom: Vec<PipelineId>,
12789
}
12890
```
12991

130-
Or perhaps we should enforce that all custom pipelines must group its supported primitives within its own layer?
131-
This needs some considering, but could be implemented further down the line as an optimization.
132-
133-
## 🎯 Implementation strategy
134-
135-
I've gone through a few ~~hundred~~ dozen implementation strategies. I'll share a few here:
136-
137-
### 🤾 Just throw a pointer at the `Renderer`
138-
139-
You can view a small, very, *very* rough and unrefined prototype of this strategy [here](https://github.com/bungoboingo/iced/tree/custom-shader/pipeline-marker/examples/custom_shader/src).
140-
141-
This example has a pointer-based approach, where the "state" of a pipeline & its associated primitive data is just
142-
stored as a heap allocated pointer within the existing `iced_wgpu::Backend`. Within a custom widget's `draw`,
143-
primitives are drawn like this:
144-
145-
```rust
146-
//...
147-
renderer.draw_primitive(Primitive::Custom {
148-
bounds,
149-
pipeline: CustomPipeline {
150-
id: self.id,
151-
init: State::init,
152-
},
153-
})
154-
```
155-
156-
Where `State::init` is a fn pointer with type signature:
157-
```rust
158-
pub init: fn(device: &wgpu::Device, format: wgpu::TextureFormat,) -> Box<dyn Renderable>
159-
```
92+
To indicate which pipelines are grouped within this layer. Or perhaps we could require that all custom pipelines are
93+
on separate layers, though that has performance implications.
16094

161-
`Renderable` refers to a trait which allows a custom pipeline to call `prepare()` (for preparing data for
162-
rendering, similar to how we are doing it in every other pipeline we support in our existing `iced_wgpu::Backend`, e.
163-
g. allocating & resizing wgpu buffers, writing uniform values, etc.).
95+
We would also need a way to cache & perform lookups for trait objects which implement `Renderable`; in my prototype
96+
I've simply used a `HashMap<PipelineId, Box<dyn Renderable>` inside of the `iced_wgpu::Backend`.
16497

165-
`Primitive::Custom` is then processed for inclusion in an `iced_wgpu::Layer`, where (if not already initialized)
166-
it's initialization (`init` above) is performed & added to a lookup map in the `iced_wgpu::Backend`. Then, come
167-
render time, if there are any `custom_primitives` within the `iced_wgpu::Layer`, we simply do a lookup for its
168-
pipeline pointer & call `prepare()` and `render()` as needed.
98+
Then, when rendering during frame presentation, we simply perform a lookup for the `PipelineId`s contains within the
99+
`Layer`, and perform their `prepare()` and `render()` methods. Done!
169100

170-
**Pros of this strategy:**
101+
**Pros of this design:**
171102

172103
- Simple to integrate into existing Iced infrastructure without major refactors.
173104
- Performance is acceptable
174105

175-
**Cons of this strategy:**
106+
**Cons of this design:**
176107

177108
- Not flexible
178109
- Even with preparing this very simple example I found myself needing to adjust the `Renderable` trait to give me
@@ -185,30 +116,19 @@ pipeline pointer & call `prepare()` and `render()` as needed.
185116
Overall I'm pretty unhappy with this implementation strategy, and feel as though it's too narrow for creating a truly
186117
flexible & modular system of adding custom shaders & pipelines to Iced.
187118

188-
### 🎨 Custom Shader Widget
119+
### 🎨 #2: Custom Shader Widget
189120

190-
Similar to how we currently have `Canvas` in Iced, this strategy would involve creating a custom widget which is
191-
dependent on `wgpu` that has its own `Program` where a user can define how to render their own custom primitive.
121+
The internals for this custom shader widget are very similar to the previous strategy; the main difference is that
122+
internally, *we* would create the custom primitive which holds the pointer to the pipeline data, not the user. The
123+
other difference is that the `Program` trait is merged with the `Renderable` trait, and that we create the widget
124+
implementation for the user, no custom widget required.
192125

193-
A custom shader widget might have a method that is defined like:
126+
Other concepts must be added, like the concept of `Time` in a render pass. In my prototype, I've implemented it at
127+
the `wgpu::Backend` level, but in its final form we would need to shift it up to the `Compositor` level, I believe.
128+
It's exposed to the user as a simple `Duration`, which is calculated from the difference between when the
129+
`iced_wgpu::Backend` is initialized up until that frame.
194130

195-
```rust
196-
pub trait Program {
197-
type State: Default + 'static;
198-
199-
fn render(
200-
&self,
201-
state: &Self::State,
202-
device: &wgpu::Device,
203-
encoder: &mut wgpu::CommandEncoder,
204-
//...
205-
);
206-
```
207-
208-
Or something similar, which, when implemented, would allow a user to define how to render their custom `State`. I
209-
found that with this strategy, a `Primtive::Custom` wrapper of some kind was still needed, which ended up being
210-
pretty similar to the previous strategy and just replacing `iced_graphics::Renderable` with
211-
`iced_graphics::custom::Program`, so I did not finish a fully flushed out prototype.
131+
There may be other information needed in the `Program` trait which is discovered as the implementation evolves.
212132

213133
**Pros:**
214134

@@ -217,64 +137,52 @@ pretty similar to the previous strategy and just replacing `iced_graphics::Rende
217137

218138
And, like the previous strategy:
219139
- Simple to integrate into existing Iced infrastructure without major refactors.
220-
- Performance is acceptable, but worse than previous strategy
140+
- Performance is acceptable
221141

222142
**Cons:**
223143
- Same cons as the previous strategy; very little flexibility, users must shoehorn their pipeline code to fit into
224-
this very specific trait `Program` provided by Iced.
225-
- When I was prototyping this out, I found it nearly impossible to do this implementation without doing some kind of
226-
reflection with `Program::State` in addition to the required dynamic dispatching. This could possibly not be a
227-
real con as there might be a different, more performant way to do it!
144+
this very specific trait `Program` provided by Iced.
228145

229146
### 🔠 Multiple Backend Support for Compositors
230147

231-
I have no prototype to speak of this with strategy; it will involve a good amount of restructuring, possibly some
232-
codegen for performance reasons, and some intermediate data structures added to the compositor. That being said, I
233-
believe this is more along the lines of a "correct" solution for integrating custom shaders & pipelines into Iced as
234-
it allows the most flexibility & feels the least hacky.
235-
236-
This strategy involves adding support for multiple `Backend`s per `Compositor`. See the diagram below for a rough
237-
outline of how it would work:
238-
239-
![](diagram.png)
240-
241-
Every `Backend` (probably should be renamed to something more appropriate, like `Pipelines` or `PipelineManager` or
242-
something for clarity) would be responsible for its own primitive type that it can support. In the case of
243-
`iced_wgpu::Backend`, this would be the `iced_graphics::Primitive` enum. The command encoder would be passed down
244-
into every backend for recording before being submitted to the GPU.
245-
246-
This would require a few new concepts added to the wgpu `Compositor`:
148+
Internally, this design is the most complex and requires the most changes to Iced, but I don't think it's so wildly
149+
complex that it would be hard to maintain! This design would require a few new concepts added to the wgpu `Compositor`:
247150

248151
💠 **Primitive Queue**
249152

250-
There must be a backend-aware queue which keeps track of the actual ordering of how primitives should be
251-
rendered across all backends. I believe this could be implemented fairly easily either by having each `Backend` keep
252-
track of its own queue and having some data structure delegate at the appropriate moment with some form of marker
253-
indicating that we need to start rendering on another `Backend`. Some kind of order-tracking data structure is
153+
There must be a backend-aware queue which keeps track of the actual ordering of how primitives should be
154+
rendered across all backends. I believe this could be implemented fairly easily either by having each `Backend` keep
155+
track of its own queue and having some data structure delegate at the appropriate moment with some form of marker
156+
indicating that we need to start rendering on another `Backend`. Some kind of order-tracking data structure is
254157
essential for ensuring proper rendering order when there are multiple backends.
255158

256159
Widgets would request that their custom primitives be added to this queue when calling `renderer.draw_primitive()`.
257160

258161
👨‍💼 **Backend "Manager"**
259162

260-
This would essentially be responsible for initializing all the backends (lazily, perhaps!) & delegating the proper
261-
primitives to the multiple `Backend`s for rendering. This would be initialized with the `Compositor` on application
163+
This would essentially be responsible for initializing all the backends (lazily, perhaps!) & delegating the proper
164+
primitives to the multiple `Backend`s for rendering. This would be initialized with the `Compositor` on application
262165
start.
263166

167+
👨‍💻 **Declarative backends!() macro**
168+
169+
This would be initialized in `Application::run()` as a parameter, or could be exposed somewhere else potentially
170+
(perhaps as an associated type of `Application`?). I haven't super thoroughly thought it through, but my initial
171+
idea is to have it return a `backend::Manager` from its `TokenStream` which would be moved into the `Compositor`.
172+
264173
**Pros:**
265174
- Flexible, users can do whatever they want with their own custom `Backend`.
266175
- Modular & additive; users can create a custom `Backend` library with their own primitives that it supports that
267176
can be initialized with the `Compositor`.
268-
- For users wanting to use a custom primitive from another library, or one they made, they would use it very
269-
similarly to how you use currently supported `Primitive`s in Iced, which would feel intuitive.
177+
- For users wanting to use a custom primitive from another library, or one they made, they would use it exactly how
178+
you use currently supported `Primitive`s in Iced, which would feel intuitive.
270179

271180
**Cons:**
272-
- Doing this strategy performantly without creating a bunch of trait objects might be challenging! At least just
273-
from thinking about it for a few days I've not come up with that many ideas other than using a hefty amount of
274-
codegen via generics or declarative macros.
181+
- Would involve a hefty amount of codegen to do performantly
275182
- This would be quite a heavy refactor for the `iced_wgpu::Compositor`!
276-
- This would (possibly?) preclude custom primitives being grouped together with other backend's primitives in
277-
its own `Layer` for transformations, scaling, etc. which might be undesirable.
183+
- This design would preclude custom primitives being clipped together with other backend's primitives in
184+
its own `Layer` for transformations, scaling, etc. which might be undesirable. There might be a way to implement
185+
this within the `backend::Manager`, however!
278186

279187
### 🤔 Other Ideas
280188

@@ -370,4 +278,6 @@ and use seamlessly as part of Iced's widget tree is the ultimate form of customi
370278

371279
### If you made it to the end, congratulations! 🥳
372280

373-
Now, let's discuss!
281+
Let's go forward and render some scuffed ice(d) cubes!
282+
283+
![](scuffed_iced_cubes.mov)

0 commit comments

Comments
 (0)