-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom WGPU Shaders & Pipelines RFC #23
Conversation
dca01b6
to
f350571
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoa, you really went down the rabbit hole!
I like the two first strategies the most (the widget could be built on top of the first one, I believe). The multiple backends seems to shift the complexity to the compositor, but adds a bunch of new ideas as well that seem hard to justify for now.
Overall, I feel we are focusing too much on the implementation details! The RFC focuses a lot on custom primitives and internals, but it's unclear how users will actually leverage this. What is the API that iced
(the root level crate) will expose to allow implementing custom shaders? This is what the guide-level explanation is meant to tackle. Primitives, queues, backends, reflection, etc. are internal concepts the user should not have to worry about, ideally.
Think about a Canvas
. You implement Program
and use the Frame
API to produce a list of Geometry
and that's about it. Users are unaware of lyon
, tessellation, mesh primitives, triangle pipeline, antialiasing, blit pipeline, etc. These are all internals that are part of the implementation, but are unnecessary ideas to use the widget.
That's what I would try to figure out first. How can a user render a rotating 3D cube inside of a widget? What is the simplest set of ideas the they will have to deal with? Do we have to expose the idea of a primitive? Or can we conveniently hide that? Do we have to expose wgpu
? What will happen when the wgpu
feature is disabled? Which approach is fun and clean?
Once we figure out what we want to offer, then we can focus on the implementation more. Basically, how do we make it happen?
ce04681
to
1aadf4c
Compare
Updated this with more focus on the actual API exposed to users. I moved each design into its own file for ease of reading. |
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
1aadf4c
to
f2712e1
Compare
Great! The design documents are the kind of stuff I expected! The The custom backend approach exposes a bunch of concepts like backends and layers to the user. Let's try to avoid that as much as possible. I think the shader widget approach makes the most sense. It's the simplest approach by far, with a single Now, let's iterate!
These are some of the questions that arise when looking at the design. Bold part is maybe the most important! We will probably need to iterate further as we start polishing it! |
I think that
I agree that things could be removed. What exactly a user might need is a bit ill defined, but if we adjust
I think it could also include Re:
I've updated In terms of how an // In an example application..
struct Example {
cubes: Cubes,
slider_value: f32,
}
enum Message {
Cubes(CubesMessage),
}
impl Application for Example {
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Cubes(cubes_msg) => {
self.cubes.update(cubes_msg)
}
}
}
fn view(&self) -> Element<Message> {
column![
slider(1.0..=100.0, self.slider_value, Message::Cubes(cube::Message::CubeSizeChanged(self.slider_value))),
self.cubes.view().map(Message::Cubes),
].into()
}
}
mod cubes {
pub struct Cubes {
cube_size: f32,
cubes_buffer: wgpu::Buffer,
//..
}
enum Message {
CubeSizeChanged(f32),
}
impl Cubes {
pub fn view(&self) -> Element<CubeMessage> {
custom::Shader::new(self)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
pub fn update(&mut self, message: Message) -> Command<Message> {
match message {
CubeSizeChanged(size) => {
self.cube_size = size;
}
}
}
}
impl custom::Program<Message> for Cubes {
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
transformation: Transformation,
scale: f32,
) {
queue.write_buffer(&self.cubes_buffer, bytemuck::bytes_of(&Cubes::new(self.cubes_size)); //or whatever
}
}
} I believe this should be fairly intuitive to users who have used a
No, we do not. After thinking about it more, since we are boxing the
Yes. This is a better solution than just always updating it in
Yes! Can just make it After all the feedback, a second iteration of this custom widget pub trait Program<Message> {
type State: Default + 'static;
fn update(
&self,
_state: &mut Self::State,
_event: custom::Event,
_bounds: Rectangle,
_cursor: custom::Cursor, //or true cursor availability..? soon(tm)?
) -> (event::Status, Option<Message>) {
(event::Status::Ignored, None)
}
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: custom::Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
transformation: Transformation,
scale: f32,
);
fn render(
&self,
_encoder: &mut wgpu::CommandEncoder,
_device: &wgpu::Device,
_target: &wgpu::TextureView,
_scale_factor: f32,
_target_size: Size<u32>,
_bounds: Rectangle<u32>,
) -> Option<RedrawRequest> {
None
}
} Where a user could make a pub struct Shader<Message, P: Program<Message>> {
width: Length,
height: Length,
program: P,
}
impl<Message, P: Program<Message>> Shader<Message, P> {
pub fn new(program: P) -> Self {
Self {
width: Length::Fill,
height: Length::Fill,
program,
}
}
//...
} I will need to think about how to pass that |
Another thought I just had would be to force users to render into their own texture which is the size of their |
Awesome! Getting simpler!
An additional dynamic dispatch call for a fairly rare primitive shouldn't be a big deal. Every single
I'd just render all the
In the new example, how will
Nested messages and self.cubes.view(self.slider_value) Message variants should not be generally accessed from outside of the module that defines them and, in this case, we only need the message to synchronize values. Both these things seem to point towards the idea that a
All the pipeline primitives use the recommended approach by Let's try to keep it simple for now. I know it reduces the scope and the capabilities considerably, but we can think about multiple rendering targets later. |
If we force the user to use our current render pass this is a potentially huge limitation. Any 3D shader for example would need to order its triangles back to front before rendering instead of using a depth buffer. The ability to create a shader with multiple passes (like for rendering water) would be impossible. Ideally I think we should have a system where users can piggy-back on to the existing render pass if they're doing more simple rendering work, or create their own. This could probably be a 2nd iteration, but something we should consider pretty soon afterwards. I can't think of any other method of doing this other than providing a second Maybe it would make sense to have a "simple" version of the custom shader program, and a more "advanced" custom shader program? I'm working on an updated example for Cubes using the aforementioned agreed upon changes, will post back with answers to other questions! |
After a design convo today with @hecrj, the current implementation for the custom shader widget has been updated! These changes address the main issue that the original design had, which was that communicating across widgets required an intermediary message to sync states. Now things are much more seamless. You can see a new (rough) example in practice here. Updated program trait: /// The state and logic of a custom `Shader` widget.
///
/// A [`Program`] can mutate internal state and produce messages for an application.
pub trait Program<Message> {
/// The internal state of the [`Program`].
type State;
/// The type of primitive this [`Program`] can render.
type Primitive: custom::Primitive; //new!
/// Update the internal [`State`] of the [`Program]. This can be used to reflect state changes
/// based on mouse & other events. You can use the [`Shell`] to publish messages, request a
/// redraw for the window, etc. which can be useful for animations.
fn update(
&mut self,
_state: &mut Self::State,
_event: Event,
_bounds: Rectangle,
_cursor: mouse::Cursor,
_shell: &mut Shell<'_, Message>,
) -> event::Status {
event::Status::Ignored
}
/// Returns the [`Primitive`] to be rendered.
fn draw(&self, _state: &Self::State) -> Self::Primitive; // new!
/// Returns the internal [`State`] of the [`Program`].
fn state(&self) -> &Self::State; //TODO?
/// Returns the current mouse interaction of the [`Program`].
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> mouse::Interaction {
mouse::Interaction::default()
}
} The associated /// A set of methods which allows a [`Primitive`] to be rendered.
pub trait Primitive: Debug + 'static {
/// Processes the [`Primitive`], allowing for GPU buffer allocation.
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_size: Size<u32>,
storage: &mut custom::Storage, //new!
);
/// Renders the [`Primitive`].
fn render(
&self,
storage: &custom::Storage,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
target_size: Size<u32>,
encoder: &mut wgpu::CommandEncoder,
);
} Similar to See how a user might implement this impl custom::Primitive for cubes::Primitive {
fn prepare(
&self,
format: wgpu::TextureFormat,
device: &wgpu::Device,
queue: &wgpu::Queue,
target_size: Size<u32>,
storage: &mut custom::Storage,
) {
if let Some(pipeline) = storage.get_mut_or_init::<Pipeline>(|| { // or something like this; it's awkward atm
Pipeline::new(device, format, target_size)
}) {
pipeline.update(
device,
queue,
target_size,
&self.uniforms,
self.cubes.len(),
&self.raw_cubes(),
);
}
}
fn render(
&self,
storage: &Storage,
bounds: Rectangle<u32>,
target: &wgpu::TextureView,
_target_size: Size<u32>,
encoder: &mut wgpu::CommandEncoder,
) {
if let Some(pipeline) = storage.get::<Pipeline>() {
pipeline.render(target, encoder, bounds, self.cubes.len() as u32)
}
}
} There is some leeway here for the user to violate the contract we give them, e.g. they could just choose to draw outside the A user can implement this impl<Message> Program<Message> for Cubes {
type State = ();
type Primitive = cube::Primitive;
fn draw(&self, _state: &Self::State) -> Self::Primitive {
cube::Primitive::new(self.size, &self.origins, &self.camera, self.time)
}
} And create their custom shader like they would any other widget:
You can see a working implementation of this design with this scuffed example: cubes_cubes_cubes.mov |
Rendered