@@ -55,124 +55,55 @@ hops? Could we do this with a minimal unsafe abstraction layer? Should we even d
5555![ ] ( silvia.jpeg )
5656
5757As 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
12286pub 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.
185116Overall I'm pretty unhappy with this implementation strategy, and feel as though it's too narrow for creating a truly
186117flexible & 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
218138And, 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
254157essential for ensuring proper rendering order when there are multiple backends.
255158
256159Widgets 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
262165start.
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