tessera_ui_basic_components/pipelines/image/
pipeline.rs

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