Skip to main content

tessera_ui_basic_components/pipelines/image/
pipeline.rs

1use std::collections::HashMap;
2
3use encase::{ShaderType, UniformBuffer};
4use glam::Vec4;
5use tessera_ui::{
6    PxPosition, PxSize,
7    renderer::drawer::pipeline::{DrawContext, DrawablePipeline},
8    wgpu,
9};
10
11use super::command::{ImageCommand, ImageData};
12
13#[derive(ShaderType)]
14struct ImageUniforms {
15    rect: Vec4,
16    is_bgra: u32,
17}
18
19struct ImageResources {
20    bind_group: wgpu::BindGroup,
21    uniform_buffer: wgpu::Buffer,
22}
23
24/// Pipeline for rendering images in UI components.
25///
26/// # Example
27/// ```rust,ignore
28/// use tessera_ui_basic_components::pipelines::image::ImagePipeline;
29/// let pipeline = ImagePipeline::new(&device, &config, sample_count);
30/// ```
31pub struct ImagePipeline {
32    pipeline: wgpu::RenderPipeline,
33    bind_group_layout: wgpu::BindGroupLayout,
34    resources: HashMap<ImageData, ImageResources>,
35}
36
37impl ImagePipeline {
38    /// Create a new ImagePipeline.
39    pub fn new(
40        device: &wgpu::Device,
41        config: &wgpu::SurfaceConfiguration,
42        pipeline_cache: Option<&wgpu::PipelineCache>,
43        sample_count: u32,
44    ) -> Self {
45        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
46            label: Some("Image Shader"),
47            source: wgpu::ShaderSource::Wgsl(include_str!("image.wgsl").into()),
48        });
49
50        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
51            entries: &[
52                wgpu::BindGroupLayoutEntry {
53                    binding: 0,
54                    visibility: wgpu::ShaderStages::FRAGMENT,
55                    ty: wgpu::BindingType::Texture {
56                        multisampled: false,
57                        view_dimension: wgpu::TextureViewDimension::D2,
58                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
59                    },
60                    count: None,
61                },
62                wgpu::BindGroupLayoutEntry {
63                    binding: 1,
64                    visibility: wgpu::ShaderStages::FRAGMENT,
65                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
66                    count: None,
67                },
68                wgpu::BindGroupLayoutEntry {
69                    binding: 2,
70                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
71                    ty: wgpu::BindingType::Buffer {
72                        ty: wgpu::BufferBindingType::Uniform,
73                        has_dynamic_offset: false,
74                        min_binding_size: None,
75                    },
76                    count: None,
77                },
78            ],
79            label: Some("texture_bind_group_layout"),
80        });
81
82        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
83            label: Some("Image Pipeline Layout"),
84            bind_group_layouts: &[&bind_group_layout],
85            push_constant_ranges: &[],
86        });
87
88        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
89            label: Some("Image Render Pipeline"),
90            layout: Some(&pipeline_layout),
91            vertex: wgpu::VertexState {
92                module: &shader,
93                entry_point: Some("vs_main"),
94                buffers: &[],
95                compilation_options: Default::default(),
96            },
97            fragment: Some(wgpu::FragmentState {
98                module: &shader,
99                entry_point: Some("fs_main"),
100                targets: &[Some(wgpu::ColorTargetState {
101                    format: config.format,
102                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
103                    write_mask: wgpu::ColorWrites::ALL,
104                })],
105                compilation_options: Default::default(),
106            }),
107            primitive: wgpu::PrimitiveState::default(),
108            depth_stencil: None,
109            multisample: wgpu::MultisampleState {
110                count: sample_count,
111                mask: !0,
112                alpha_to_coverage_enabled: false,
113            },
114            multiview: None,
115            cache: pipeline_cache,
116        });
117
118        Self {
119            pipeline,
120            bind_group_layout,
121            resources: HashMap::new(),
122        }
123    }
124
125    /// Return existing resources for `data` or create them.
126    fn get_or_create_resources(
127        &mut self,
128        device: &wgpu::Device,
129        queue: &wgpu::Queue,
130        config: &wgpu::SurfaceConfiguration,
131        data: &ImageData,
132    ) -> &ImageResources {
133        self.resources.entry(data.clone()).or_insert_with(|| {
134            Self::create_image_resources(device, queue, config, &self.bind_group_layout, data)
135        })
136    }
137
138    /// Compute the ImageUniforms for a given command size and position.
139    fn compute_uniforms(
140        start_pos: PxPosition,
141        size: PxSize,
142        config: &wgpu::SurfaceConfiguration,
143    ) -> ImageUniforms {
144        // Convert pixel positions/sizes into normalized device coordinates and size ratios.
145        let rect = [
146            (start_pos.x.0 as f32 / config.width as f32) * 2.0 - 1.0
147                + (size.width.0 as f32 / config.width as f32),
148            (start_pos.y.0 as f32 / config.height as f32) * -2.0 + 1.0
149                - (size.height.0 as f32 / config.height as f32),
150            size.width.0 as f32 / config.width as f32,
151            size.height.0 as f32 / config.height as f32,
152        ]
153        .into();
154
155        let is_bgra = matches!(
156            config.format,
157            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
158        );
159
160        ImageUniforms {
161            rect,
162            is_bgra: if is_bgra { 1 } else { 0 },
163        }
164    }
165
166    // Create GPU resources for an image. Kept as a single helper to avoid duplicating
167    // GPU setup logic while keeping `draw` concise.
168    fn create_image_resources(
169        device: &wgpu::Device,
170        queue: &wgpu::Queue,
171        config: &wgpu::SurfaceConfiguration,
172        layout: &wgpu::BindGroupLayout,
173        data: &ImageData,
174    ) -> ImageResources {
175        let texture_size = wgpu::Extent3d {
176            width: data.width,
177            height: data.height,
178            depth_or_array_layers: 1,
179        };
180        let diffuse_texture = device.create_texture(&wgpu::TextureDescriptor {
181            size: texture_size,
182            mip_level_count: 1,
183            sample_count: 1,
184            dimension: wgpu::TextureDimension::D2,
185            format: config.format,
186            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
187            label: Some("diffuse_texture"),
188            view_formats: &[],
189        });
190
191        queue.write_texture(
192            wgpu::TexelCopyTextureInfo {
193                texture: &diffuse_texture,
194                mip_level: 0,
195                origin: wgpu::Origin3d::ZERO,
196                aspect: wgpu::TextureAspect::All,
197            },
198            &data.data,
199            wgpu::TexelCopyBufferLayout {
200                offset: 0,
201                bytes_per_row: Some(4 * data.width),
202                rows_per_image: Some(data.height),
203            },
204            texture_size,
205        );
206
207        let diffuse_texture_view =
208            diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
209        let diffuse_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
210            address_mode_u: wgpu::AddressMode::ClampToEdge,
211            address_mode_v: wgpu::AddressMode::ClampToEdge,
212            address_mode_w: wgpu::AddressMode::ClampToEdge,
213            mag_filter: wgpu::FilterMode::Linear,
214            min_filter: wgpu::FilterMode::Nearest,
215            mipmap_filter: wgpu::FilterMode::Nearest,
216            ..Default::default()
217        });
218
219        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
220            label: Some("Image Uniform Buffer"),
221            size: ImageUniforms::min_size().get(),
222            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
223            mapped_at_creation: false,
224        });
225
226        let diffuse_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
227            layout,
228            entries: &[
229                wgpu::BindGroupEntry {
230                    binding: 0,
231                    resource: wgpu::BindingResource::TextureView(&diffuse_texture_view),
232                },
233                wgpu::BindGroupEntry {
234                    binding: 1,
235                    resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
236                },
237                wgpu::BindGroupEntry {
238                    binding: 2,
239                    resource: uniform_buffer.as_entire_binding(),
240                },
241            ],
242            label: Some("diffuse_bind_group"),
243        });
244
245        ImageResources {
246            bind_group: diffuse_bind_group,
247            uniform_buffer,
248        }
249    }
250}
251
252impl DrawablePipeline<ImageCommand> for ImagePipeline {
253    fn draw(&mut self, context: &mut DrawContext<ImageCommand>) {
254        context.render_pass.set_pipeline(&self.pipeline);
255
256        for (command, size, start_pos) in context.commands.iter() {
257            // Use the extracted helper to obtain or create GPU resources.
258            let resources = self.get_or_create_resources(
259                context.device,
260                context.queue,
261                context.config,
262                &command.data,
263            );
264
265            // Use the extracted uniforms computation helper (dereference borrowed tuple elements).
266            let uniforms = Self::compute_uniforms(*start_pos, *size, context.config);
267
268            let mut buffer = UniformBuffer::new(Vec::new());
269            buffer.write(&uniforms).unwrap();
270            context
271                .queue
272                .write_buffer(&resources.uniform_buffer, 0, &buffer.into_inner());
273
274            context
275                .render_pass
276                .set_bind_group(0, &resources.bind_group, &[]);
277            context.render_pass.draw(0..6, 0..1);
278        }
279    }
280}